This commit is contained in:
2017-02-10 00:33:25 +00:00
commit a3e6ce3fb5
99 changed files with 11317 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
@import '../variables.css';
.root {
display: flex;
flex-flow: row wrap;
flex-direction: column;
width: 100%;
margin: 0;
padding: 20px;
}
@media (--sm-viewport) {
.root {
width: 50%;
}
}
.formGroup {
position: relative;
margin-bottom: 15px;
}
.label {
display: inline-block;
margin-bottom: 5px;
max-width: 100%;
font-weight: 700;
}
.input {
display: block;
box-sizing: border-box;
padding: 10px 16px;
width: 100%;
height: 46px;
outline: 0;
border: 1px solid var(--colour-grey);
border-radius: 0;
background-color: #fff;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
color: #616161;
font-size: 18px;
line-height: 1.3333333;
transition: border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s,
background-color ease-in-out 0.15s;
}
.input:focus {
border-color: var(--colour-tertiary);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 116, 194, 0.6);
}
.inputSuccess {
background-color: var(--colour-green);
}
.inputError {
background-color: var(--colour-red);
}
.rsvpDetails {
display: none;
}
.rsvpDetailsVisible {
display: block;
}
.iconContainer {
display: inline-block;
position: absolute;
top: 35px;
right: 15px;
}
.rsvpSummary {
position: fixed;
right: 10px;
bottom: 10px;
left: 10px;
padding: 10px;
text-align: center;
opacity: 0;
border-radius: 0;
background-color: #fff;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
color: #616161;
font-size: 18px;
pointer-events: none;
transition: opacity ease-in-out .4s;
}
.rsvpSummarySuccess {
opacity: 1;
background-color: var(--colour-green);
}

View File

@@ -0,0 +1,261 @@
import React, { PropTypes } from 'react';
import cx from 'classnames';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { MdCheck, MdError } from 'react-icons/lib/md';
import s from './RsvpCard.css';
import { starters, mains } from '../../config';
class RsvpCard extends React.Component {
static propTypes = {
className: PropTypes.string,
person: PropTypes.shape({
key: PropTypes.string,
firstname: PropTypes.string,
lastname: PropTypes.string,
email: PropTypes.string,
password: PropTypes.string,
completed: PropTypes.boolean,
attending: PropTypes.boolean,
dietary: PropTypes.string,
starter: PropTypes.string,
main: PropTypes.string,
}).isRequired,
};
state = {
detailsVisible: this.props.person.attending,
saveAttending: this.props.person.attending ? 'saved' : null,
saveStarter: this.props.person.starter !== '-1' ? 'saved' : null,
saveMain: this.props.person.main !== '-1' ? 'saved' : null,
saveDietary: this.props.person.dietary ? 'saved' : null,
};
isRSVPComplete(prop, value) {
function isOK(str) {
return (str === 'saved' || str === 'success');
}
if (prop === 'attending' && value === 'false') { return true; }
return (isOK(this.state.saveAttending) &&
isOK(this.state.saveStarter) &&
isOK(this.state.saveMain));
}
saveData(prop, value) {
const stateKey = `save${prop.charAt(0).toUpperCase()}${prop.slice(1)}`;
const state = {};
fetch('/rsvp/save', {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: this.props.person.key,
prop,
value,
}),
credentials: 'include',
}).then(res => res.json())
.then(data => {
state[stateKey] = data.success ? 'success' : 'error';
this.setState(state);
if (this.isRSVPComplete(prop, value)) {
this.setState({
summaryState: 'success',
summary: `Thanks ${this.props.person.firstname}, your RSVP has been received`,
});
if (!this.state.saveCompleted) {
this.setState({ saveSompleted: true });
this.saveData('completed', true);
}
setTimeout(() => {
this.setState({
summaryState: null,
summary: null,
});
}, 5000);
}
state[stateKey] = data.success ? 'saved' : 'error';
setTimeout(() => {
this.setState(state);
}, 500);
}).catch(() => {
state[stateKey] = 'error';
});
}
handleAttendingChange = (event) => {
this.saveData('attending', event.target.value);
this.setState({
detailsVisible: event.target.value === 'true',
attendingResponse: event.target.value === 'true' ? '👍' : '☹️',
});
};
handleDietaryBlur = (event) => {
this.saveData('dietary', event.target.value);
};
handleStarterChange = (event) => {
this.saveData('starter', event.target.value);
};
handleMainChange = (event) => {
this.saveData('main', event.target.value);
};
handleFormSubmit = (event) => {
event.preventDefault();
};
render() {
return (
<div className={cx(s.root, this.props.className)}>
<h2>
{this.props.person.firstname} {this.props.person.lastname}
<span> {this.state.attendingResponse}</span>
</h2>
<form onSubmit={this.handleFormSubmit}>
<div className={s.formGroup}>
<label className={s.label} htmlFor="attending">
Attending:
</label>
{this.props.person.attending === null ?
<select
className={[
s.input,
this.state.saveAttending === 'success' ? s.inputSuccess : null,
this.state.saveAttending === 'error' ? s.inputError : null,
].join(' ')}
id="attending"
name="attending"
defaultValue="-1"
onChange={this.handleAttendingChange}
>
<option value="-1" disabled>Will you be attending?</option>
<option value="true">
Yes
</option>
<option value="false">
No
</option>
</select>
:
<select
className={[
s.input,
this.state.saveAttending === 'success' ? s.inputSuccess : null,
this.state.saveAttending === 'error' ? s.inputError : null,
].join(' ')}
id="attending"
name="attending"
defaultValue={this.props.person.attending === true ? 'true' : 'false'}
onChange={this.handleAttendingChange}
>
<option value="true">
Yes
</option>
<option value="false">
No
</option>
</select>
}
<div className={s.iconContainer}>
{this.state.saveAttending === 'saved' ? <MdCheck size={30} color="green" /> : null}
{this.state.saveAttending === 'error' ? <MdError size={30} color="red" /> : null}
</div>
</div>
<div className={[s.rsvpDetails, this.state.detailsVisible ? s.rsvpDetailsVisible : null].join(' ')}>
<div className={s.formGroup}>
<label className={s.label} htmlFor="starter">
Starter:
</label>
<select
className={[
s.input,
this.state.saveStarter === 'success' ? s.inputSuccess : null,
this.state.saveStarter === 'error' ? s.inputError : null,
].join(' ')}
id="starter"
name="starter"
defaultValue={this.props.person.starter}
onChange={this.handleStarterChange}
>
{this.props.person.starter === '-1' ? <option value="-1" disabled>Please select a starter</option> : null}
{starters.map((starter, index) => (
<option key={index} value={index}>{starter}</option>
))}
</select>
<div className={s.iconContainer}>
{this.state.saveStarter === 'saved' ? <MdCheck size={30} color="green" /> : null}
{this.state.saveStarter === 'error' ? <MdError size={30} color="red" /> : null}
</div>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="main">
Main:
</label>
<select
className={[
s.input,
this.state.saveMain === 'success' ? s.inputSuccess : null,
this.state.saveMain === 'error' ? s.inputError : null,
].join(' ')}
id="main"
name="main"
defaultValue={this.props.person.main}
onChange={this.handleMainChange}
>
{this.props.person.main === '-1' ? <option value="-1" disabled>Please select a main course</option> : null}
{mains.map((main, index) => (
<option key={index} value={index}>{main}</option>
))}
</select>
<div className={s.iconContainer}>
{this.state.saveMain === 'saved' ? <MdCheck size={30} color="green" /> : null}
{this.state.saveMain === 'error' ? <MdError size={30} color="red" /> : null}
</div>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="dietary">
Dietary requirements (if any):
</label>
<input
className={[
s.input,
this.state.saveDietary === 'success' ? s.inputSuccess : null,
this.state.saveDietary === 'error' ? s.inputError : null,
].join(' ')}
id="dietary"
type="text"
name="dietary"
defaultValue={this.props.person.dietary}
onBlur={this.handleDietaryBlur}
/>
<div className={s.iconContainer}>
{this.state.saveDietary === 'saved' ? <MdCheck size={30} color="green" /> : null}
{this.state.saveDietary === 'error' ? <MdError size={30} color="red" /> : null}
</div>
</div>
</div>
</form>
<div
className={[
s.rsvpSummary,
this.state.summaryState === 'success' ? s.rsvpSummarySuccess : null,
].join(' ')}
>
{this.state.summary}
</div>
</div>
);
}
}
export default withStyles(s)(RsvpCard);

View File

@@ -0,0 +1,6 @@
{
"name": "RsvpCard",
"version": "0.0.0",
"private": true,
"main": "./RsvpCard.js"
}