Reference Source

src/components/views/addbike-view.js

import React, { Component } from 'react';
import { StyleSheet, Text, View, Button, PixelRatio, TouchableOpacity, Image, Alert, ScrollView, FlatList, ActivityIndicator, TouchableWithoutFeedback, KeyboardAvoidingView } from 'react-native';
import { Icon } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';
import { HeaderBackButton } from 'react-navigation';
import { TextInput } from 'react-native-paper';
import SectionedMultiSelect from 'react-native-sectioned-multi-select';

import { styles, text, edit_styles } from './stylesheets/edit-styles';

import BaseView from './view';
import SafeArea from './helpers/safearea';
import HandleBack from './helpers/handleback';
import ImageCarousel from './helpers/imagecarousel';
import AddBikePresenter from '../presenters/addbike-presenter';
import ImageUtil from '../../util/imageutility';
import TimeUtil from '../../util/timeutility';

const colours = require('../../assets/colours/colours.json');

const NO_DATA = 'NO-DATA';
const UNIQUE_COLOUR_KEY = 'name'; // A unique key for the colours for the sectioned list

/**
 * Class for the AddBike view
 * @extends BaseView
 */
class AddBikeView extends BaseView {
	/**
	 * Creates an instance of the add bike view
	 *
	 * @constructor
	 * @param {Object} props - Component properties
	 */
	constructor(props) {
		super(props);
		this.AddBikeP = new AddBikePresenter(this);
	}

	state = { // Initializing the state
		editing: false, // Checks if user is editing
		refresh: true, // Triggers a view refresh
		loaderVisible: false,
		isEditPage: false,

		inputData: [], // Input text data is at each index
		colours: colours.data, // Current colours

		currentID: '', // Current ID of the bike being edited (Edit Bike only)

		photoEntries: [], // Current photos

		selectedItems: [] // Selected colours
	};

	/**
	 * Set the navigation options, change the header to handle a back button.
	 *
	 * @return {Object} Navigation option
	 */
	static navigationOptions = ({navigation, transitioning}) => {
		const { params = {} } = navigation.state;
		const back = params._onBack ? params._onBack : () => 'default';
		const clear = params._clearData ? params._clearData : () => 'default';
		return {
			headerLeft: (<HeaderBackButton disabled={transitioning} onPress={()=>{back()}}/>),
			headerRight: (<Button disabled={transitioning} onPress={()=>{clear()}} title='Clear'/>),
			title: navigation.getParam('title', 'Add Bike') // Default title is Add Bike
		};
	}

	/**
	 * Component is about to mount, initialize the data.
	 * This function is called before componentDidMount
	 */
	componentWillMount = () => {
		this.props.navigation.setParams({
			_onBack: this._onBack,
			_clearData: this._clearData
		});

		const { navigation } = this.props;
		const data = navigation.getParam('data', 'NO-DATA');
		const viewTitle = navigation.getParam('title', 'Add Bike');

		// This can be done before the component has mounted so we do it before so data appears immediately
		this.setState({
			rawData: data,
			inputData: this.AddBikeP.getTextInputData(data, this.state.isEditPage),
			photoEntries: this.AddBikeP.getCurrentPhotos(),
			isEditPage: this.isEditBikePage(viewTitle)
		});
	}

	/**
	 * Component mounted
	 */
	componentDidMount = () => {
		this.AddBikeP.changeText(colours.data, this._renderText);

		const { navigation } = this.props;
		const data = navigation.getParam('data', 'NO-DATA');

		// This can only be done once the component has mounted since it affects other components
		this.AddBikeP.toggleColours(this.sectionedMultiSelect, data, this._onSelectedItemsChange, UNIQUE_COLOUR_KEY);
	}

	/**
	 * Component will unmount after this method is called, do any clean up here
	 * Call viewUnmounting in base class so it can do any cleanup for the view before calling the presenter destroy method
	 */
	componentWillUnmount = () => {
		this.viewUnmounting(this.AddBikeP);
	}

	/**
	 * Checks if the title of the view is edit bike, and if so, returns true.
	 *
	 * @param {string} title - The title of the view
	 * @return {Boolean} true: If the page is edit bike; false: If the page is add bike
	 */
	isEditBikePage = (title) => {
		return title === 'Edit Bike';
	}

	/**
	 * Refreshes the state of the component so new data is fetched.
	 */
	refreshState = () => {
		this.setState({ 
			refresh: !this.state.refresh
		});
	}

	/**
	 * Toggle the editing mode.
	 */
	toggleEditing = () => {
		this.setState({ editing: !editing });
	}

	/**
	 * Set the editing value to the passed in value.
	 *
	 * @param {Boolean} edit - true: user is editing; false: user is not editing
	 */
	setEditing = (edit) => {
		this.setState({ editing: edit });
	}

	/**
	 * When the back button is clicked, check if the user was editing.
	 */
	_onBack = () => {
		if (!this.state.loaderVisible) {
			this.AddBikeP.checkEditingState(this.state.editing, this.editingSuccess, this.editingFailure);
		}
	}

	/**
	 * A function to execute when the editing state is true.
	 */
	editingSuccess = () => {
		Alert.alert(
			"You're still editing!",
			"Are you sure you want to go back with your edits not saved?",
			[
				{ text: "Keep Editing", onPress: () => {}, style: "cancel" },
				{ text: "Go Back", onPress: () => this.resetAllOnBack() },
			],
			{ cancelable: false },
		);
	}

	/**
	 * A function to execute when the editing state is false.
	 */
	editingFailure = () => {
		// Clear the data just in case
		this.resetAllOnBack(); // If not editing then go back
	}

	/**
	 * Resets all the data and goes back to the bike page
	 */
	resetAllOnBack = () => {
		this._clearData();
		this.props.navigation.navigate('Bike');
	}

	/**
	 * Clears all the data
	 */
	_clearData = () => {
		if (!this.state.loaderVisible) {
			this.AddBikeP.clearPhotos();
			this.sectionedMultiSelect._removeAllItems();
			let inputData = this.AddBikeP.getTextInputData(NO_DATA, this.state.isEditPage); // inputData is a property in state
			let photoEntries = ImageUtil.getDefaultPhotos(ImageUtil.getTypes().BIKE);
			this.setState({ inputData, photoEntries });
			this.setEditing(false); // Set editing to false so user can easily go back (for clear button)
		}
	}

	/**
	 * Prompt to ask the user if they want to delete the bike
	 */
	deletePrompt = () => {
		Alert.alert(
			"Are you sure you want to delete this bike?",
			"",
			[
				{ text: "Yes", onPress: () => {this._enableLoader(); this.AddBikeP.deleteBike(this.state.currentID, this.deleteCallback)}},
				{ text: "No", onPress: () => {}, style: "cancel" },
			],
			{ cancelable: false },
		);
	}

	/**
	 * Sets a callback on what to do if there is a success or error when a bike is uploaded.
	 *
	 * @param {Boolean} success - true: Uploading successful; false: Uploading failed
	 */
	deleteCallback = (success) => {
		this._disableLoader();
		this.refreshState();
		if (success) {
			Alert.alert(
				"Bike successfully deleted!",
				"",
				[
					{ text: "Ok", onPress: () => this.resetAllOnBack(), style: "ok" },
				],
				{ cancelable: false },
			);
		} else {
			Alert.alert(
				"Bike was not able to be deleted.",
				"Please try again.",
				[
					{ text: "Ok", onPress: () => {}, style: "ok" },
				],
				{ cancelable: false },
			);
		}
	}	

	/**
	 * Render a text input item.
	 * 
	 * @param {Object} item - A list item, index - The index of the item in the data list
	 * @return {Component} A react component
	 */
	_renderItem = ({item, index}) => (
		<TextInput
			style={text.textInput}
			label={item.required ? this._renderName(item.name) : item.name} // Give required inputs a different render
			multiline={item.multiline}
			disabled={item.disabled || this.state.loaderVisible}
			value={this.state.inputData[index].text}
			onChangeText={(text) => {
					let { inputData } = this.state; // inputData is a keyword in state
					inputData[index].text = text;
					this.setState({ inputData });
					this.setEditing(true); // Now editing
				} 
			}/>
	);

	/**
	 * Renders the name of a required field.
	 *
	 * @param {string} name - The name of the field
	 * @return {Component} A react component
	 */
	_renderName = (name) => (
		<Text style={[{color: 'red'}]}>{name + " *"}</Text>
	);


	/**
	 * Extract the key from the item and index
	 *
	 * @param {Object} item - A list item
	 * @param {Number} index - The index of the item
	 */
	_keyExtractor = (item, index) => item.name;

	/**
	 * Add the new selected items to the state and update
	 *
	 * @param {List} selectedItems - List of selected items
	 */
	_onSelectedItemsChange = (selectedItems) => {
		this.setEditing(true); // Now editing
		this.setState({ selectedItems });
	} 

	/**
	 * Generates the style and colouring of the colours in the multiselect.
	 * 
	 * @param {string} colour - A colour, usually hexcode
	 * @param {string} name - The name of the item 
	 * @return {Component} A react component
	 */
	_renderText = (colour, name) => (
		<Text style={[{color: colour}, text.colourText]}>{name}</Text>
	);


	/**
	 * Get the data from the state and send an update to the presenter
	 */
	_getDataToUpdate = () => {
		if (!this.AddBikeP.checkInputs(this.state.inputData, this._inputRequirementFailure)) {
			return; // Callback is called within checkInputs so no need to call anything here
		}

		this._enableLoader(); // Activates spinning loader
		this.refreshState();
		
		// We like it tight so pack it together neatly
		let updateData = {
			currentID: this.state.currentID,
			inputTextData: this.state.inputData, 
			selectedColours: this.state.selectedItems, 
			picture: this.state.photoEntries
		};

		this.AddBikeP.update(updateData, this.alertCallback);
	}

	/**
	 * Enables the loader.
	 */
	_enableLoader = () => { this.setState({ loaderVisible: true }); }

	
	/**
	 * Disables the loader.
	 */
	_disableLoader = () => { this.setState({ loaderVisible: false }); }

	/**
	 * Alert for requirement input failure
	 */
	_inputRequirementFailure = (names) => {
		const joiner = names.length > 1 ? " are" : " is";
		Alert.alert(
			"Required (*) inputs cannot be blank.",
			names.join(', ') + joiner + " required.",
			[
				{ text: "Ok", onPress: () => {}, style: "ok" },
			],
			{ cancelable: false },
		);
	}

	/**
	 * Sets a callback on what to do if there is a success or error when a bike is uploaded.
	 *
	 * @param {Boolean} success - true: Uploading successful; false: Uploading failed
	 */
	alertCallback = (success) => {
		this._disableLoader();
		this.refreshState();
		if (success) {
			Alert.alert(
				"Bike successfully uploaded!",
				"",
				[
					{ text: "Ok", onPress: () => this.resetAllOnBack(), style: "ok" },
				],
				{ cancelable: false },
			);
		} else {
			Alert.alert(
				"Bike was not able to be uploaded.",
				"Please try again.",
				[
					{ text: "Ok", onPress: () => {}, style: "ok" },
				],
				{ cancelable: false },
			);
		}
	}

	/**
	 * DON'T USE THIS METHOD UNLESS ABSOLUTELY NECESSARY.
	 * Force a refresh of the view.
	 */
	_forceRefresh = () => {
		this.forceUpdate();
	}

	/**
	 * Renders items to the screen
	 *
	 * @return {Component} 
	 */
	render() {
		return (
			
			<KeyboardAvoidingView
				style={styles.container}
				behavior="padding"
				enabled>
				<HandleBack onBack={this._onBack}>
					<SafeArea/>
					<View style={styles.container}>
						<ScrollView 
							contentContainerStyle={edit_styles.contentContainer}
							keyboardShouldPersistTaps='always'
							keyboardDismissMode='interactive'>

							<ImageCarousel 
								loading={this.state.loaderVisible}
								photos={this.state.photoEntries}
								selected={(id) => {this.AddBikeP.selectPhotoTapped(ImagePicker, this.setEditing, id, this.state.photoEntries)}} />
							
							{/* List of text inputs */}
							<FlatList
								style={edit_styles.flatList}
								data={this.AddBikeP.getTextInputData(NO_DATA, this.state.isEditPage)}
								extraData={this.state}
								keyExtractor={this._keyExtractor}
								renderItem={this._renderItem}/>
						
							{/* List of colours */}
							{/* colors attribute makes the 'Confirm' button flip from red to green if a colour is selected */}
							<SectionedMultiSelect
								style={text.textInput}
								items={this.state.colours}
								displayKey='text_component'
								uniqueKey={UNIQUE_COLOUR_KEY}
								showRemoveAll
								colors={{ primary: this.state.selectedItems.length ? 'forestgreen' : 'crimson' }}
								selectText='Colours'
								modalWithSafeAreaView={true}
								showDropDowns={true}
								filterItems={this.AddBikeP.filterItems}
								onSelectedItemsChange={this._onSelectedItemsChange}
								selectedItems={this.state.selectedItems}
								ref={(SectionedMultiSelect) => this.sectionedMultiSelect = SectionedMultiSelect}
								/>

							{/* Submit button */}
							<TouchableOpacity style={[edit_styles.submitTouchable, {marginBottom: this.state.isEditPage ? 0 : 10}]}>
								<Button
									title={this.state.isEditPage ? 'Save' : 'Submit'}
									disabled={this.state.loaderVisible}
									onPress={() => this._getDataToUpdate()}/>
							</TouchableOpacity>

							{/* TODO : Add delete button */}
							{
								this.state.isEditPage &&
								<View> 
									<View style={{flexDirection: 'row', marginTop: 20}}> 
										<View style={edit_styles.deleteInline} /> 
											<Text style={edit_styles.delete}>Delete Bike</Text> 
										<View style={edit_styles.deleteInline} /> 
									</View>
									
									<TouchableOpacity style={edit_styles.deleteTouchable} onPress={() => 'default'}>
										<Button
											title='Delete'
											disabled={this.state.loaderVisible}
											onPress={() => this.deletePrompt()}/>
									</TouchableOpacity>
								</View>
							}

							{/* Spinning loading circle */}
							{
								this.state.loaderVisible &&
								<View style={edit_styles.loading} pointerEvents="none">
									<ActivityIndicator size='large' color="#0000ff" />
								</View>
							}
						</ScrollView>
					</View>
					<SafeArea/>
				</HandleBack>
				</KeyboardAvoidingView>
			
		);
	}
}

export default AddBikeView;