- Breaking up the profile

This commit is contained in:
Mike Fitzpatrick
2019-08-17 02:45:57 -04:00
parent c146884636
commit cc8442b0b2
12 changed files with 557 additions and 192 deletions

View File

@@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { setNomDeBid } from '../../../actions/profile.js';
import { getNomDeBid, isGeneratedNomDeBid } from '../../../selectors/profile.js';
import EditNomDeBid from './EditNomDeBid.js';
const matchStateToProps = (state) => ({
isGeneratedNomDeBid: isGeneratedNomDeBid(state),
nomDeBid: getNomDeBid(state),
});
const matchDispatchToProps = (dispatch) => ({
updateNomDeBid: (nomDeBid) => dispatch(setNomDeBid(nomDeBid)),
});
export default connect(
matchStateToProps,
matchDispatchToProps,
)(EditNomDeBid);

View File

@@ -0,0 +1,79 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Text, TextInput, View } from 'react-native';
import { Avatar } from 'react-native-elements';
import { getNomAvailaibility } from '../../../api/profile.js';
import styles from '../Profile.styles.js';
const STRINGS = {
ABOUT_GENERATED_NOM:
'You currently have a generated Nom De Bid - the alias other bidders will know you as - why not personalize it?',
CHECK_AVAILABILITY: 'check availability',
NOM_EXPLANATION:
"Selecting a nom de bid allows you to bid anonymously - or not. By default, we'll use your first initial and last name.",
ONLY_SET_ONCE: 'This can only be set once!',
PLACEHOLDER: 'nom de bid',
SUBMIT_NOM: 'Set Nom De Bid',
};
export default function EditNomDeBid({
isGeneratedNomDeBid,
isStandalone,
nomDeBid,
updateNomDeBid,
}) {
const [newNom, setNomDeBid] = useState(isGeneratedNomDeBid || !nomDeBid ? '' : nomDeBid);
const [isNomValid, setValidNom] = useState(null);
const _handleEndEditing = (updateOnValid = false) => () => {
getNomAvailaibility(newNom).then((result) => {
setValidNom(result.available);
if (updateOnValid && result.available) {
updateNomDeBid(newNom);
}
});
};
const explanationString = isGeneratedNomDeBid
? `${STRINGS.ABOUT_GENERATED_NOM} ${STRINGS.ONLY_SET_ONCE}`
: `${STRINGS.NOM_EXPLANATION} ${STRINGS.ONLY_SET_ONCE}`;
return (
<View style={styles.profileFormWrap}>
<Text style={styles.hintText}>{explanationString}</Text>
<TextInput
autoCapitalize="none"
onChangeText={(text) => setNomDeBid(text)}
onEndEditing={!isStandalone && _handleEndEditing(false)}
placeholder={STRINGS.PLACEHOLDER}
style={[styles.textInput, styles.requiredInput]}
value={newNom}
/>
{isNomValid === false && (
<Text style={{color:'red'}}>Nom De Bid is taken!</Text>
)}
{isNomValid === true && (
<Text style={{color:'green'}}>Nom De Bid is available!</Text>
)}
<Button
title={isStandalone ? STRINGS.CHECK_AVAILABILITY : STRINGS.SUBMIT_NOM}
onPress={_handleEndEditing(true)}
/>
</View>
);
}
EditNomDeBid.propTypes = {
isGeneratedNomDeBid: PropTypes.bool,
isStandalone: PropTypes.bool,
nomDeBid: PropTypes.string,
updateNomDeBid: PropTypes.func.isRequired,
};
EditNomDeBid.defaultProps = {
isGeneratedNomDeBid: false,
isStandalone: false,
nomDeBid: null,
};

View File

@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Text, TextInput, View } from 'react-native';
import { getEmailAvailability } from '../../../api/profile.js';
import styles from '../Profile.styles.js';
const STRINGS = {
CONTINUE: 'continue',
PLACEHOLDER: 'email address',
REGISTERED: `You've already registered!`,
RESET_PASSWORD: 'reset password',
};
export default function EmailInput({ email, handleValidEmailEntry, isRequired, showContinueButton }) {
const [emailValue, setEmail] = useState(email);
const [isEmailAvailable, setEmailAvailable] = useState(null);
const _handleContinueButtonPress = () => {
};
const _handleEndEditing = () => {
if (!emailValue.match(/.+\@.+\..+/)) {
Alert.alert(
'Invalid Email',
`Hmmm... You entered '${emailValue}' and something doesn't look quite right...`,
);
return;
}
getEmailAvailability(emailValue)
.then((result) => {
const { available } = result;
console.log(`EmailInput._validateEmail => getEmailAvailability(${emailValue}):`, result);
if (available) {
handleValidEmailEntry(emailValue);
}
setEmailAvailable(available);
});
};
return (
<View style={[styles.sectionWrap, styles.password, isRequired && styles.requiredWrap]}>
<TextInput
autoCapitalize="none"
keyboardType="email-address"
onChangeText={setEmail}
onEndEditing={showContinueButton ? null : _handleEndEditing}
placeholder={STRINGS.PLACEHOLDER}
style={[styles.textInput, isRequired && styles.requiredInput]}
value={emailValue || ''}
/>
{isEmailAvailable === false && (
<View style={styles.emailTaken}>
<Text style={{color:'red'}}>{STRINGS.REGISTERED}</Text>
<Button title={STRINGS.RESET_PASSWORD} onPress={() => {}} />
</View>
)}
{showContinueButton && !isEmailAvailable && (
<Button
onPress={_handleEndEditing}
title={STRINGS.CONTINUE}
/>
)}
</View>
);
}
EmailInput.propTypes = {
email: PropTypes.string,
handleValidEmailEntry: PropTypes.func.isRequired,
isRequired: PropTypes.bool,
showContinueButton: PropTypes.bool,
};
EmailInput.defaultProps = {
email: null,
isRequired: false,
showContinueButton: false,
}

View File

@@ -0,0 +1,53 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Text, TextInput, View } from 'react-native';
import styles from '../Profile.styles.js';
const STRINGS = {
HINT: `Password must be at least 8 characters and contain a minimum of one lowercase and one uppercase letter, a number, and a symbol. Spaces are allowed. Be memorable and secure!`,
MATCH: `That's a match!`,
NO_MATCH: `Well that's not a match...`,
PLACEHOLDER_PASSWORD: 'password',
PLACEHOLDER_VERIFY: 're-enter password',
};
export default function PasswordInput({ handleValidPasswordEntry }) {
const [password, setPassword] = useState(null);
const [doPasswordsMatch, setPasswordMatch] = useState(null);
const _doesVerificationPasswordMatch = (text) => {
if (!password || password.length < text.length) {
setPasswordMatch(null);
return;
}
const result = password === text;
handleValidPasswordEntry(result && password);
setPasswordMatch(result);
};
return (
<View style={[styles.sectionWrap, styles.password, styles.requiredWrap]}>
<Text style={styles.hintText}>{STRINGS.HINT}</Text>
<TextInput
onChangeText={setPassword}
placeholder={STRINGS.PLACEHOLDER_PASSWORD}
secureTextEntry
style={[styles.textInput, styles.requiredInput]}
/>
<TextInput
onChangeText={_doesVerificationPasswordMatch}
placeholder={STRINGS.PLACEHOLDER_VERIFY}
secureTextEntry
style={[styles.textInput, styles.requiredInput]}
/>
{doPasswordsMatch === true && <Text style={{ color: 'green' }}>{STRINGS.MATCH}</Text>}
{doPasswordsMatch === false && <Text style={{ color: 'red' }}>{STRINGS.NO_MATCH}</Text>}
</View>
);
}
PasswordInput.propTypes = {
handleValidPasswordEntry: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,122 @@
import { List } from 'immutable';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Button, Picker, Text, TextInput, View } from 'react-native';
import { PHONE_TYPE_DEFAULT, PHONE_TYPES } from '../../../constants/constants.js';
import PhoneListItem from './PhoneListItem.js';
import styles from '../Profile.styles.js';
const defaultState = {
editingIndex: null,
isEditing: false,
newPhone: null,
newPhoneType: PHONE_TYPE_DEFAULT,
};
export default class PhoneListInput extends Component {
static get propTypes() {
return {
handleAdd: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
handleEdit: PropTypes.func,
phones: PropTypes.instanceOf(List).isRequired,
};
};
static get defaultProps() {
return {
handleEdit: null,
};
};
constructor(props) {
super(props);
this.state = { ...defaultState };
this.handleAdd = this.handleAdd.bind(this);
this.handleEdit = this.handleEdit.bind(this);
this.handleEditCancel = this.handleEditCancel.bind(this);
}
handleAdd() {
const { phones } = this.props;
const { newPhone, newPhoneType } = this.state;
this.props.handleAdd(phones.push({ number: newPhone, label: newPhoneType }));
this.setState(defaultState);
}
handleEdit(index) {
const { phones } = this.props;
const { newPhone, newPhoneType } = this.state;
this.props.handleEdit(phones.set(index, { number: newPhone, label: newPhoneType }));
this.setState(defaultState);
}
handleEditStart(index) {
const toBeEdited = this.props.phones.get(index);
this.setState({
editingIndex: index,
isEditing: true,
newPhone: toBeEdited.get('number'),
newPhoneType: toBeEdited.get('label'),
});
}
handleEditCancel(index) {
this.setState(defaultState);
}
render() {
const { phones } = this.props;
const { isEditing, newPhone, newPhoneType } = this.state;
const numbersTitle = 'Numbers';
return (
<View>
<Text style={[styles.groupHeading, styles.requiredLabel]}>{numbersTitle}</Text>
{phones !== null && phones.size > 0 && (
<View style={styles.phoneList}>
{phones.map((phone, index) =>
<PhoneListItem
hideDelete={phones.size < 2}
index={index}
label={phone.label}
number={phone.number}
handleDelete={this.props.handleDelete}
handleEdit={this.props.handleEdit ? this.handleEdit : null}
handleEditStart={this.handleEditStart}
/>
)}
</View>
)}
<TextInput
onChangeText={(text) => this.setState({ newPhone: text })}
placeholder="phone number"
style={[styles.textInput, styles.requiredInput]}
value={this.state.newPhone}
/>
<Picker
onValueChange={(type, index) => this.setState({ newPhoneType: type })}
selectedValue={this.state.newPhoneType}
>
{PHONE_TYPES.map((type) => <Picker.Item key={type.value} {...type} />)}
</Picker>
<Button
disabled={!this.state.newPhone && !this.state.newPhoneType}
onPress={isEditing ? () => this.handleEdit(index) : this.handleAdd}
title={isEditing ? 'update' : 'add number'}
/>
{isEditing && (
<Button
onPress={this.handleEditCancel}
title="cancel"
/>
)}
</View>
);
}
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-native-elements';
import { Alert, Text, View } from 'react-native';
import styles from '../Profile.styles.js';
export default function PhoneListItem({
hideDelete,
index,
label,
number,
handleDelete,
handleEdit,
handleEditStart,
}) {
const _onLongPressDelete = () => {
handleDelete(index);
};
const _onPressDelete = (isLongPress = false) => () => {
if (isLongPress) {
handleDelete(index);
return;
}
Alert.alert(
'Delete this number?',
'Are you sure you want to delete this number?',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'OK', onPress: () => handleDelete(index) },
],
{ cancelable: true },
);
};
return (
<View style={styles.listItem}>
<View style={styles.listValue}>
<Text style={styles.value}>{number}</Text>
<Text style={styles.label}>{label}</Text>
</View>
<View style={styles.listActions}>
{handleEdit !== null && <Button accessibilityLabel={`Edit`} onPress={() => handleEditStart()} />}
{!hideDelete && (
<Button
accessibilityLabel={`Delete`}
onLongPress={_onPressDelete(true)}
onPress={_onPressDelete()}
/>
)}
</View>
</View>
);
}
PhoneListItem.propTypes = {
hideDelete: PropTypes.bool,
index: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
number: PropTypes.string.isRequired,
handleDelete: PropTypes.func.isRequired,
handleEdit: PropTypes.func,
handleEditStart: PropTypes.func.isRequired,
};
PhoneListItem.defaultProps = {
hideDelete: false,
handleEdit: null,
};