Use common class to handle section list display

This commit is contained in:
keplyx 2019-07-26 14:14:01 +02:00
parent 3ec7614061
commit 861b655fe5
9 changed files with 339 additions and 414 deletions

2
App.js
View file

@ -31,7 +31,7 @@ export default class App extends React.Component<Props, State> {
}
/**
* Loads data before components are mounted, like fonts and themes
* Loads FetchedData before components are mounted, like fonts and themes
* @returns {Promise}
*/
async componentWillMount() {

View file

@ -0,0 +1,122 @@
// @flow
import * as React from 'react';
import WebDataManager from "../utils/WebDataManager";
import {Container, Content, H2} from "native-base";
import CustomHeader from "./CustomHeader";
import {SectionList, RefreshControl, View} from "react-native";
type Props = {
navigation: Object,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
fetchedData: Object,
machinesWatched : Array<Object>
};
export default class FetchedDataSectionList extends React.Component<Props, State> {
webDataManager : WebDataManager;
constructor() {
super();
this.webDataManager = new WebDataManager(this.getFetchUrl());
}
state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
machinesWatched : [],
};
getFetchUrl() {
return "";
}
getHeaderTranslation() {
return "Header";
}
getUpdateToastTranslations () {
return ["whoa", "nah"];
}
/**
* Refresh the FetchedData on first screen load
*/
componentDidMount() {
this._onRefresh();
}
_onRefresh = () => {
this.setState({refreshing: true});
this.webDataManager.readData().then((fetchedData) => {
this.setState({
fetchedData: fetchedData,
refreshing: false,
firstLoading: false
});
this.webDataManager.showUpdateToast(this.getUpdateToastTranslations()[0], this.getUpdateToastTranslations()[1]);
});
};
getRenderItem(item: Object, section : Object, data : Object) {
return <View />;
}
getRenderSectionHeader(title: String) {
return <View />;
}
/**
* Create the dataset to be used in the list from the data fetched
* @param fetchedData {Object}
* @return {Array}
*/
createDataset(fetchedData : Object) : Array<Object> {
return [];
}
/**
* What item field should be used as a key in the list
* @param item {Object}
* @return {*}
*/
getKeyExtractor(item : Object) {
return item.id;
}
render() {
const nav = this.props.navigation;
const dataset = this.createDataset(this.state.fetchedData);
return (
<Container>
<CustomHeader navigation={nav} title={this.getHeaderTranslation()}/>
<Content padder>
<SectionList
sections={dataset}
keyExtractor={(item) => this.getKeyExtractor(item)}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh}
/>
}
renderSectionHeader={({section: {title}}) =>
this.getRenderSectionHeader(title)
}
renderItem={({item, section}) =>
this.getRenderItem(item, section, dataset)
}
style={{minHeight: 300, width: '100%'}}
/>
</Content>
</Container>
);
}
}

View file

@ -1,44 +1,15 @@
// @flow
import * as React from 'react';
import {Image, View, Linking, RefreshControl, FlatList} from 'react-native';
import {Container, Content, Text, Button, Card, CardItem, Left, Body, Thumbnail, H2, Toast} from 'native-base';
import CustomHeader from '../components/CustomHeader';
import {Image, Linking} from 'react-native';
import {Text, Button, Card, CardItem, Left, Body, Thumbnail} from 'native-base';
import i18n from "i18n-js";
import CustomMaterialIcon from '../components/CustomMaterialIcon';
import FetchedDataSectionList from "../components/FetchedDataSectionList";
const ICON_AMICALE = require('../assets/amicale.png');
type Props = {
navigation: Object,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
data: Object,
};
const FB_URL = "https://graph.facebook.com/v3.3/amicale.deseleves/posts?fields=message%2Cfull_picture%2Ccreated_time%2Cpermalink_url&access_token=EAAGliUs4Ei8BAGwHmg7SNnosoEDMuDhP3i5lYOGrIGzZBNeMeGzGhpUigJt167cKXEIM0GiurSgaC0PS4Xg2GBzOVNiZCfr8u48VVB15a9YbOsuhjBqhHAMb2sz6ibwOuDhHSvwRZCUpBZCjmAW12e7RjWJp0jvyNoYYvIQbfaLWi3Nk2mBc";
let test_data = [
{
title: "News de l'Amicale",
date: "June 15, 2019",
thumbnail: require("../assets/amicale.png"),
image: require("../assets/drawer-cover.png"),
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus congue sapien leo, ac dignissim odio dignissim sit amet. Quisque tempor, turpis sed scelerisque faucibus, dolor tortor porta sapien, eget tincidunt ante elit et ex. Sed sagittis dui non nisl aliquet viverra. Integer quis convallis enim, sit amet auctor ante. Praesent quis lacinia magna. Sed augue lacus, congue eu turpis vel, consectetur pellentesque nulla. Maecenas blandit diam odio, et finibus urna egestas non. Quisque congue finibus efficitur. Sed pretium mauris nec neque mattis, eu condimentum velit ultrices. Fusce eleifend porttitor nunc non suscipit. Aenean porttitor feugiat ipsum sit amet interdum. Maecenas tempor felis non tempus vehicula. Suspendisse sit amet eros neque. ",
link: "https://en.wikipedia.org/wiki/Main_Page"
},
{
title: "Lancement de la super appli de la mort avec un titre super long",
date: "June 14, 2019",
thumbnail: require("../assets/amicale.png"),
image: require("../assets/image-missing.png"),
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus congue sapien leo, eget tincidunt ante elit et ex. Sed sagittis dui non nisl aliquet viverra. Integer quis convallis enim, sit amet auctor ante. Praesent quis lacinia magna. Sed augue lacus, congue eu turpis vel, consectetur pellentesque nulla. Maecenas blandit diam odio, et finibus urna egestas non. Quisque congue finibus efficitur. Sed pretium mauris nec neque mattis, eu condimentum velit ultrices. Fusce eleifend porttitor nunc non suscipit.",
link: "https://en.wikipedia.org/wiki/Central_Link"
}
];
const NAME_AMICALE = 'Amicale INSA Toulouse';
const FB_URL = "https://graph.facebook.com/v3.3/amicale.deseleves/posts?fields=message%2Cfull_picture%2Ccreated_time%2Cpermalink_url&&date_format=U&access_token=EAAGliUs4Ei8BAGwHmg7SNnosoEDMuDhP3i5lYOGrIGzZBNeMeGzGhpUigJt167cKXEIM0GiurSgaC0PS4Xg2GBzOVNiZCfr8u48VVB15a9YbOsuhjBqhHAMb2sz6ibwOuDhHSvwRZCUpBZCjmAW12e7RjWJp0jvyNoYYvIQbfaLWi3Nk2mBc";
/**
* Opens a link in the device's browser
@ -51,68 +22,56 @@ function openWebLink(link) {
/**
* Class defining the app's home screen
*/
export default class HomeScreen extends React.Component<Props, State> {
export default class HomeScreen extends FetchedDataSectionList {
state = {
refreshing: false,
firstLoading: true,
data: {},
};
async readData() {
try {
let response = await fetch(FB_URL);
let responseJson = await response.json();
this.setState({
data: responseJson
});
} catch (error) {
console.log('Could not read data from server');
console.log(error);
this.setState({
data: {}
});
}
getHeaderTranslation() {
return i18n.t("screens.home");
}
isDataObjectValid() {
return Object.keys(this.state.data).length > 0;
getUpdateToastTranslations () {
return [i18n.t("homeScreen.listUpdated"),i18n.t("homeScreen.listUpdateFail")];
}
_onRefresh = () => {
this.setState({refreshing: true});
this.readData().then(() => {
this.setState({
refreshing: false,
firstLoading: false
});
if (this.isDataObjectValid()) {
Toast.show({
text: i18n.t('proxiwashScreen.listUpdated'),
buttonText: 'OK',
type: "success",
duration: 2000
})
} else {
Toast.show({
text: i18n.t('proxiwashScreen.listUpdateFail'),
buttonText: 'OK',
type: "danger",
duration: 4000
})
getKeyExtractor(item : Object) {
return item.id;
}
});
};
getRenderItem(item: Object) {
createDataset(fetchedData : Object) {
let data = [];
if (fetchedData.data !== undefined)
data = fetchedData.data;
return [
{
title: '',
data: data,
extraData: super.state
}
];
}
getFetchUrl() {
return FB_URL;
}
/**
* Converts a dateString using Unix Timestamp to a formatted date
* @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date
*/
static getFormattedDate(dateString: string) {
let date = new Date(Number.parseInt(dateString) * 1000);
return date.toLocaleString();
}
getRenderItem(item: Object, section : Object, data : Object) {
return (
<Card style={{flex: 0}}>
<CardItem>
<Left>
<Thumbnail source={ICON_AMICALE}/>
<Body>
<Text>Amicale</Text>
<Text note>{item.created_time}</Text>
<Text>{NAME_AMICALE}</Text>
<Text note>{HomeScreen.getFormattedDate(item.created_time)}</Text>
</Body>
</Left>
</CardItem>
@ -139,38 +98,4 @@ export default class HomeScreen extends React.Component<Props, State> {
);
}
/**
* Refresh the data on first screen load
*/
componentDidMount() {
this._onRefresh();
}
render() {
const nav = this.props.navigation;
let displayData = this.state.data.data;
return (
<Container>
<CustomHeader navigation={nav} title={i18n.t('screens.home')}/>
<Content padder>
<FlatList
data={displayData}
extraData={this.state}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh}
/>
}
renderItem={({item}) =>
this.getRenderItem(item)
}
style={{minHeight: 300, width: '100%'}}
/>
</Content>
</Container>
);
}
}

View file

@ -1,11 +1,10 @@
// @flow
import * as React from 'react';
import {RefreshControl, SectionList} from 'react-native';
import {Container, Text, ListItem, Left, Right, Body, Badge, Toast, H2} from 'native-base';
import CustomHeader from "../../components/CustomHeader";
import {Badge, Body, H2, Left, ListItem, Right, Text} from 'native-base';
import i18n from "i18n-js";
import CustomMaterialIcon from "../../components/CustomMaterialIcon";
import FetchedDataSectionList from "../../components/FetchedDataSectionList";
const DATA_URL = "https://etud.insa-toulouse.fr/~vergnet/appli-amicale/dataProximo.json";
@ -17,145 +16,73 @@ const typesIcons = {
Default: "information-outline",
};
type Props = {
navigation: Object
}
type State = {
refreshing: boolean,
firstLoading: boolean,
data: Object,
};
/**
* Class defining the main proximo screen. This screen shows the different categories of articles
* offered by proximo.
*/
export default class ProximoMainScreen extends React.Component<Props, State> {
export default class ProximoMainScreen extends FetchedDataSectionList {
state = {
refreshing: false,
firstLoading: true,
data: {},
};
getFetchUrl() {
return DATA_URL;
}
getHeaderTranslation() {
return i18n.t("screens.proximo");
}
getUpdateToastTranslations() {
return [i18n.t("proximoScreen.listUpdated"), i18n.t("proximoScreen.listUpdateFail")];
}
getKeyExtractor(item: Object) {
return item.type;
}
createDataset(fetchedData: Object) {
return [
{
title: i18n.t('proximoScreen.listTitle'),
data: ProximoMainScreen.generateData(fetchedData),
extraData: super.state,
}
];
}
/**
* Generate the dataset using types and data.
* Generate the data using types and FetchedData.
* This will group items under the same type.
*
* @param types An array containing the types available (categories)
* @param data The array of articles represented by objects
* @param fetchedData The array of articles represented by objects
* @returns {Array} The formatted dataset
*/
static generateDataset(types: Array<string>, data: Array<Object>) {
static generateData(fetchedData: Object) {
let finalData = [];
if (fetchedData.types !== undefined && fetchedData.articles !== undefined) {
let types = fetchedData.types;
let articles = fetchedData.articles;
for (let i = 0; i < types.length; i++) {
finalData.push({
type: types[i],
data: []
});
for (let k = 0; k < data.length; k++) {
if (data[k]['type'].includes(types[i])) {
finalData[i].data.push(data[k]);
for (let k = 0; k < articles.length; k++) {
if (articles[k]['type'].includes(types[i])) {
finalData[i].data.push(articles[k]);
}
}
}
}
return finalData;
}
/**
* Async function reading data from the proximo website and setting the state to rerender the list
*
* @returns {Promise<void>}
*/
async readData() {
try {
let response = await fetch(DATA_URL);
let responseJson = await response.json();
if (responseJson['articles'].length !== 0 && responseJson['types'].length !== 0) {
let data = ProximoMainScreen.generateDataset(responseJson['types'], responseJson['articles']);
this.setState({
data: data
});
} else
this.setState({data: undefined});
} catch (error) {
console.error(error);
}
}
/**
* Refresh the list on first screen load
*/
componentDidMount() {
this._onRefresh();
}
/**
* Display a loading indicator and fetch data from the internet
*
* @private
*/
_onRefresh = () => {
this.setState({refreshing: true});
this.readData().then(() => {
this.setState({
refreshing: false,
firstLoading: false
});
Toast.show({
text: i18n.t('proximoScreen.listUpdated'),
buttonText: 'OK',
type: "success",
duration: 2000
})
});
};
/**
* Renders the proximo categories list.
* If we are loading for the first time, change the data for the SectionList to display a loading message.
*
* @returns {react.Node}
*/
render() {
const nav = this.props.navigation;
const data = [
{
title: i18n.t('proximoScreen.listTitle'),
data: this.state.data,
extraData: this.state,
}
];
const loadingData = [
{
title: i18n.t('proximoScreen.loading'),
data: []
}
];
getRenderItem(item: Object, section : Object, data : Object) {
return (
<Container>
<CustomHeader navigation={nav} title={'Proximo'}/>
<SectionList
sections={this.state.firstLoading ? loadingData : data}
keyExtractor={(item, index) => item.type}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh}
/>
}
style={{minHeight: 300, width: '100%'}}
renderSectionHeader={({section: {title}}) => (
<H2 style={{textAlign: 'center', paddingVertical: 10}}>{title}</H2>
)}
renderItem={({item}) =>
<ListItem
button
thumbnail
onPress={() => {
nav.navigate('ProximoListScreen', item);
this.props.navigation.navigate('ProximoListScreen', item);
}}
>
<Left>
@ -175,10 +102,12 @@ export default class ProximoMainScreen extends React.Component<Props, State> {
<Right>
<CustomMaterialIcon icon="chevron-right"/>
</Right>
</ListItem>}
/>
</Container>
</ListItem>
);
}
getRenderSectionHeader(title: String) {
return <H2 style={{textAlign: 'center', paddingVertical: 10}}>{title}</H2>;
}
}

View file

@ -1,14 +1,13 @@
// @flow
import * as React from 'react';
import {SectionList, RefreshControl, View} from 'react-native';
import {Body, Container, Icon, Left, ListItem, Right, Text, Toast, H2, Button} from 'native-base';
import CustomHeader from "../components/CustomHeader";
import {AsyncStorage, View} from 'react-native';
import {Body, Button, H2, Icon, Left, ListItem, Right, Text} from 'native-base';
import ThemeManager from '../utils/ThemeManager';
import NotificationsManager from '../utils/NotificationsManager';
import i18n from "i18n-js";
import {AsyncStorage} from 'react-native'
import CustomMaterialIcon from "../components/CustomMaterialIcon";
import FetchedDataSectionList from "../components/FetchedDataSectionList";
import NotificationsManager from "../utils/NotificationsManager";
const DATA_URL = "https://etud.insa-toulouse.fr/~vergnet/appli-amicale/dataProxiwash.json";
const WATCHED_MACHINES_PREFKEY = "proxiwash.watchedMachines";
@ -27,37 +26,25 @@ let stateStrings = {};
let stateColors = {};
type Props = {
navigation: Object,
};
type State = {
refreshing: boolean,
firstLoading: boolean,
data: Object,
machinesWatched: Array<Object>
};
/**
* Class defining the app's proxiwash screen. This screen shows information about washing machines and
* dryers, taken from a scrapper reading proxiwash website
*/
export default class ProxiwashScreen extends React.Component<Props, State> {
export default class ProxiwashScreen extends FetchedDataSectionList {
state = {
refreshing: false,
firstLoading: true,
data: {},
fetchedData: {},
machinesWatched : [],
};
/**
* Creates machine state parameters using current theme and translations
*
* @param props
*/
constructor(props: Props) {
super(props);
constructor() {
super();
let colors = ThemeManager.getInstance().getCurrentThemeVariables();
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor;
@ -72,34 +59,20 @@ export default class ProxiwashScreen extends React.Component<Props, State> {
stateStrings[MACHINE_STATES.ERREUR] = i18n.t('proxiwashScreen.states.error');
}
/**
* Check if the data object contains valid entries
*
* @returns {boolean}
*/
isDataObjectValid() {
return Object.keys(this.state.data).length > 0;
getFetchUrl() {
return DATA_URL;
}
/**
* Read the data from the proxiwash scrapper and set it to current state to reload the screen
*
* @returns {Promise<void>}
*/
async readData() {
try {
let response = await fetch(DATA_URL);
let responseJson = await response.json();
this.setState({
data: responseJson
});
} catch (error) {
console.log('Could not read data from server');
console.log(error);
this.setState({
data: {}
});
getHeaderTranslation() {
return i18n.t("screens.proxiwash");
}
getUpdateToastTranslations() {
return [i18n.t("proxiwashScreen.listUpdated"), i18n.t("proxiwashScreen.listUpdateFail")];
}
getKeyExtractor(item: Object) {
return item.number;
}
/**
@ -116,43 +89,6 @@ export default class ProxiwashScreen extends React.Component<Props, State> {
});
}
/**
* Refresh the data on first screen load
*/
componentDidMount() {
this._onRefresh();
}
/**
* Show the refresh indicator and wait for data to be fetched from the scrapper
*
* @private
*/
_onRefresh = () => {
this.setState({refreshing: true});
this.readData().then(() => {
this.setState({
refreshing: false,
firstLoading: false
});
if (this.isDataObjectValid()) {
Toast.show({
text: i18n.t('proxiwashScreen.listUpdated'),
buttonText: 'OK',
type: "success",
duration: 2000
})
} else {
Toast.show({
text: i18n.t('proxiwashScreen.listUpdateFail'),
buttonText: 'OK',
type: "danger",
duration: 4000
})
}
});
};
/**
* Get the time remaining based on start/end time and done percent
*
@ -247,15 +183,30 @@ export default class ProxiwashScreen extends React.Component<Props, State> {
}) !== undefined;
}
createDataset(fetchedData: Object) {
return [
{
title: i18n.t('proxiwashScreen.dryers'),
data: fetchedData.dryers === undefined ? [] : fetchedData.dryers,
extraData: super.state
},
{
title: i18n.t('proxiwashScreen.washers'),
data: fetchedData.washers === undefined ? [] : fetchedData.washers,
extraData: super.state
},
];
}
/**
* Get list item to be rendered
*
* @param item The object containing the item's data
* @param item The object containing the item's FetchedData
* @param section The object describing the current SectionList section
* @param data The full data used by the SectionList
* @param data The full FetchedData used by the SectionList
* @returns {React.Node}
*/
renderItem(item: Object, section: Object, data: Object) {
getRenderItem(item: Object, section: Object, data: Object) {
return (
<ListItem
thumbnail
@ -293,11 +244,13 @@ export default class ProxiwashScreen extends React.Component<Props, State> {
{backgroundColor: '#ba7c1f'} : {}}
onPress={() => {
this.setupNotifications(item.number, ProxiwashScreen.getRemainingTime(item.startTime, item.endTime, item.donePercent))
}}>
}}
>
<Text>
{ProxiwashScreen.getRemainingTime(item.startTime, item.endTime, item.donePercent) + ' ' + i18n.t('proxiwashScreen.min')}
</Text>
<Icon name={this.isMachineWatched(item.number) ? 'bell-ring' : 'bell'}
<Icon
name={this.isMachineWatched(item.number) ? 'bell-ring' : 'bell'}
type={'MaterialCommunityIcons'}
style={{fontSize: 30, width: 30}}
/>
@ -311,63 +264,7 @@ export default class ProxiwashScreen extends React.Component<Props, State> {
</ListItem>);
}
/**
* Renders the machines list.
* If we are loading for the first time, change the data for the SectionList to display a loading message.
*
* @returns {react.Node}
*/
render() {
const nav = this.props.navigation;
let data = [];
if (!this.isDataObjectValid()) {
data = [
{
title: i18n.t('proxiwashScreen.error'),
data: []
}
];
} else {
data = [
{
title: i18n.t('proxiwashScreen.dryers'),
data: this.state.data.dryers === undefined ? [] : this.state.data.dryers,
extraData: this.state
},
{
title: i18n.t('proxiwashScreen.washers'),
data: this.state.data.washers === undefined ? [] : this.state.data.washers,
extraData: this.state
},
];
}
const loadingData = [
{
title: i18n.t('proxiwashScreen.loading'),
data: []
}
];
return (
<Container>
<CustomHeader navigation={nav} title={'Proxiwash'}/>
<SectionList
sections={this.state.firstLoading ? loadingData : data}
keyExtractor={(item) => item.number}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh}
/>
}
renderSectionHeader={({section: {title}}) => (
<H2 style={{textAlign: 'center', paddingVertical: 10}}>{title}</H2>
)}
renderItem={({item, section}) =>
this.renderItem(item, section, data)
}
/>
</Container>
);
getRenderSectionHeader(title: String) {
return <H2 style={{textAlign: 'center', paddingVertical: 10}}>{title}</H2>;
}
}

View file

@ -43,7 +43,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
};
/**
* Gets data from preferences before rendering components
* Gets FetchedData from preferences before rendering components
*
* @returns {Promise<void>}
*/

View file

@ -2,6 +2,8 @@
"screens": {
"home": "Home",
"planning": "Planning",
"proxiwash": "Proxiwash",
"proximo": "Proximo",
"settings": "Settings",
"about": "About"
},
@ -22,6 +24,10 @@
"30": "30 min"
}
},
"homeScreen": {
"listUpdated": "List updated!",
"listUpdateFail": "Error while updating list"
},
"aboutScreen": {
"appstore": "See on the Appstore",
"playstore": "See on the Playstore",

View file

@ -2,6 +2,8 @@
"screens": {
"home": "Accueil",
"planning": "Planning",
"proxiwash": "Proxiwash",
"proximo": "Proximo",
"settings": "Paramètres",
"about": "À Propos"
},
@ -22,6 +24,10 @@
"30": "30 min"
}
},
"homeScreen": {
"listUpdated": "List mise à jour!",
"listUpdateFail": "Erreur lors de la mise à jour de la liste"
},
"aboutScreen": {
"appstore": "Voir sur l'Appstore",
"playstore": "Voir sur le Playstore",

40
utils/WebDataManager.js Normal file
View file

@ -0,0 +1,40 @@
import {Toast} from "native-base";
export default class WebDataManager {
FETCH_URL : string;
lastDataFetched : Object = {};
constructor(url) {
this.FETCH_URL = url;
}
async readData() {
let fetchedData : Object = {};
try {
let response = await fetch(this.FETCH_URL);
fetchedData = await response.json();
} catch (error) {
console.log('Could not read FetchedData from server');
console.log(error);
}
this.lastDataFetched = fetchedData;
return fetchedData;
}
isDataObjectValid() {
return Object.keys(this.lastDataFetched).length > 0;
}
showUpdateToast(successString, errorString) {
let isSuccess = this.isDataObjectValid();
Toast.show({
text: isSuccess ? successString : errorString,
buttonText: 'OK',
type: isSuccess ? "success" : "danger",
duration: 2000
})
}
}