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,13 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.container {
margin: 0 auto;
padding: 0 0 40px;
max-width: var(--max-content-width);
}

36
src/routes/admin/Admin.js Normal file
View File

@@ -0,0 +1,36 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Admin.css';
import Link from '../../components/Link';
class Admin extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
people: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
firstname: PropTypes.string,
lastname: PropTypes.string,
email: PropTypes.string,
password: PropTypes.string,
attending: PropTypes.boolean,
})).isRequired,
};
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.props.title}</h1>
<div>
<h2>People</h2>
<p>{this.props.people.length} people</p>
<p>{this.props.people.filter(person => person.completed).length} responses</p>
<Link to="/admin/people">Manage people</Link>
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Admin);

41
src/routes/admin/index.js Normal file
View File

@@ -0,0 +1,41 @@
import React from 'react';
import Layout from '../../components/Layout';
import fetchQL from '../../core/fetchQL';
import { starters, mains } from '../../config';
const title = 'Admin';
export default {
path: '/admin',
async action() {
const resp = await fetchQL('{getAllPersons{key,firstname,lastname,email,password,completed,starter,main}}');
const food = {
starters: starters.map(name => ({ name, total: 0 })),
mains: mains.map(name => ({ name, total: 0 })),
};
let people = await resp.json();
if (people.data && people.data.getAllPersons) {
people = people.data.getAllPersons;
} else {
people = [];
}
const Admin = await new Promise((resolve) => {
require.ensure([], (require) => resolve(require('./Admin').default), 'admin');
});
return {
title,
chunk: 'admin',
component: <Layout><Admin
title={title}
people={people}
food={food}
/></Layout>,
};
},
};

View File

@@ -0,0 +1,77 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.container {
margin: 0 auto;
padding: 0 0 40px;
max-width: var(--max-content-width);
}
.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);
}
.button {
display: block;
box-sizing: border-box;
margin: 0;
padding: 10px 16px;
width: 100%;
outline: 0;
border: 1px solid var(--colour-secondary);
border-radius: 0;
background: var(--colour-primary);;
color: #fff;
text-align: center;
text-decoration: none;
font-size: 18px;
line-height: 1.3333333;
cursor: pointer;
}
.button:hover {
background: var(--colour-secondary);
}
.button:focus {
border-color: var(--colour-tertiary);
box-shadow: 0 0 8px var(--colour-secondary);
}

View File

@@ -0,0 +1,112 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdminPeople.css';
import Link from '../../components/Link';
class AdminPeople extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
people: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
firstname: PropTypes.string,
lastname: PropTypes.string,
completed: PropTypes.boolean,
email: PropTypes.string,
password: PropTypes.string,
attending: PropTypes.boolean,
})).isRequired,
};
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.props.title}</h1>
{this.props.people.length === 0 ? <p> No people yet ☹️</p> :
<table>
<thead>
<tr>
<td>Name</td>
<td>Email</td>
<td>Password</td>
</tr>
</thead>
<tbody>
{this.props.people.map(person => (
<tr key={person.key}>
<td><Link to={`/admin/person/${person.key}`}>{person.firstname} {person.lastname}</Link></td>
<td>{person.email}</td>
<td><Link to={`/rsvp/${person.password}`}>{person.password}</Link></td>
<td>
<form method="post" action="/admin/person/delete">
<input type="hidden" name="key" value={person.key} />
<button className={s.button} type="submit">
Delete
</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
}
<h2>Add Person</h2>
<form method="post" action="/admin/person">
<div className={s.formGroup}>
<label className={s.label} htmlFor="firstname">
First name:
</label>
<input
className={s.input}
id="firstname"
type="text"
name="firstname"
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="lastname">
Last name:
</label>
<input
className={s.input}
id="lastname"
type="text"
name="lastname"
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="email">
Email:
</label>
<input
className={s.input}
id="email"
type="text"
name="email"
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="password">
Password (blank will autogenerate):
</label>
<input
className={s.input}
id="password"
type="text"
name="password"
/>
</div>
<div className={s.formGroup}>
<button className={s.button} type="submit">
Save
</button>
</div>
</form>
</div>
</div>
);
}
}
export default withStyles(s)(AdminPeople);

View File

@@ -0,0 +1,32 @@
import React from 'react';
import Layout from '../../components/Layout';
import fetchQL from '../../core/fetchQL';
const title = 'People';
export default {
path: '/admin/people',
async action() {
const resp = await fetchQL('{getAllPersons{key,firstname,lastname,email,completed,password}}');
let people = await resp.json();
if (people.data && people.data.getAllPersons) {
people = people.data.getAllPersons;
} else {
people = [];
}
const AdminPeople = await new Promise((resolve) => {
require.ensure([], (require) => resolve(require('./AdminPeople').default), 'adminPeople');
});
return {
title,
chunk: 'admin',
component: <Layout><AdminPeople title={title} people={people} /></Layout>,
};
},
};

View File

@@ -0,0 +1,77 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.container {
margin: 0 auto;
padding: 0 0 40px;
max-width: var(--max-content-width);
}
.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);
}
.button {
display: block;
box-sizing: border-box;
margin: 0;
padding: 10px 16px;
width: 100%;
outline: 0;
border: 1px solid var(--colour-secondary);
border-radius: 0;
background: var(--colour-primary);;
color: #fff;
text-align: center;
text-decoration: none;
font-size: 18px;
line-height: 1.3333333;
cursor: pointer;
}
.button:hover {
background: var(--colour-secondary);
}
.button:focus {
border-color: var(--colour-tertiary);
box-shadow: 0 0 8px var(--colour-secondary);
}

View File

@@ -0,0 +1,101 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdminPerson.css';
class AdminPerson extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
person: PropTypes.shape({
key: PropTypes.string.isRequired,
firstname: PropTypes.string,
lastname: PropTypes.string,
completed: PropTypes.boolean,
email: PropTypes.string,
password: PropTypes.string,
attending: PropTypes.boolean,
}).isRequired,
};
state = {
firstname: this.props.person.firstname,
lastname: this.props.person.lastname,
};
handleFirstnameChange = (event) => {
this.setState({ firstname: event.target.value });
}
handleLastnameChange = (event) => {
this.setState({ lastname: event.target.value });
}
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.state.firstname} {this.state.lastname}</h1>
<form method="post" action="/admin/person_todo">
<div className={s.formGroup}>
<label className={s.label} htmlFor="firstname">
First name:
</label>
<input
className={s.input}
id="firstname"
type="text"
name="firstname"
defaultValue={this.props.person.firstname}
onChange={this.handleFirstnameChange}
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="lastname">
Last name:
</label>
<input
className={s.input}
id="lastname"
type="text"
name="lastname"
defaultValue={this.props.person.lastname}
onChange={this.handleLastnameChange}
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="email">
Email:
</label>
<input
className={s.input}
id="email"
type="text"
name="email"
defaultValue={this.props.person.email}
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="password">
Password:
</label>
<input
className={s.input}
id="password"
type="text"
name="password"
defaultValue={this.props.person.password}
/>
</div>
<div className={s.formGroup}>
<button className={s.button} type="submit">
Save
</button>
</div>
</form>
</div>
</div>
);
}
}
export default withStyles(s)(AdminPerson);

View File

@@ -0,0 +1,38 @@
import React from 'react';
import Layout from '../../components/Layout';
import fetchQL from '../../core/fetchQL';
export default {
path: '/admin/person/:key',
async action({ params }) {
if (!params.key) {
return { redirect: '/admin' };
}
const resp = await fetchQL('{getPersonByKey{key,firstname,lastname,email,completed,password,starter,main,dietary}}', {
key: params.key,
});
let person = await resp.json();
if (person.data && person.data.getPersonByKey && person.data.getPersonByKey.length === 1) {
person = person.data.getPersonByKey[0];
} else {
return { redirect: '/admin' };
}
const AdminPerson = await new Promise((resolve) => {
require.ensure([], (require) => resolve(require('./AdminPerson').default), 'adminPerson');
});
const title = `${person.firstname} ${person.lastname}`;
return {
title,
chunk: 'admin',
component: <Layout><AdminPerson title={title} person={person} /></Layout>,
};
},
};

View File

@@ -0,0 +1,47 @@
* {
line-height: 1.2;
margin: 0;
}
html {
color: #888;
display: table;
font-family: sans-serif;
height: 100%;
text-align: center;
width: 100%;
}
body {
display: table-cell;
vertical-align: middle;
padding: 2em;
}
h1 {
color: #555;
font-size: 2em;
font-weight: 400;
}
p {
margin: 0 auto;
width: 280px;
}
pre {
text-align: left;
margin-top: 2rem;
}
@media only screen and (max-width: 280px) {
body,
p {
width: 95%;
}
h1 {
font-size: 1.5em;
margin: 0 0 0.3em;
}
}

View File

@@ -0,0 +1,32 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './ErrorPage.css';
class ErrorPage extends React.Component {
static propTypes = {
error: PropTypes.object.isRequired,
};
render() {
if (process.env.NODE_ENV !== 'production') {
const { error } = this.props;
return (
<div>
<h1>{error.name}</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
</div>
);
}
return (
<div>
<h1>Error</h1>
<p>Sorry, a critical error occurred on this page.</p>
</div>
);
}
}
export { ErrorPage as ErrorPageWithoutStyle };
export default withStyles(s)(ErrorPage);

17
src/routes/error/index.js Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import ErrorPage from './ErrorPage';
export default {
path: '/error',
action({ error }) {
return {
title: error.name,
description: error.message,
component: <ErrorPage error={error} />,
status: error.status || 500,
};
},
};

73
src/routes/home/Home.css Normal file
View File

@@ -0,0 +1,73 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.formContainer {
margin: 0 auto;
max-width: 420px;
}
.formGroup {
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: #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;
}
.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);
}
.button {
display: block;
box-sizing: border-box;
margin: 0;
padding: 10px 16px;
width: 100%;
outline: 0;
border: 1px solid var(--colour-secondary);
border-radius: 0;
background: var(--colour-primary);;
color: #fff;
text-align: center;
text-decoration: none;
font-size: 18px;
line-height: 1.3333333;
cursor: pointer;
}
.button:hover {
background: var(--colour-secondary);
}
.button:focus {
border-color: var(--colour-tertiary);
box-shadow: 0 0 8px var(--colour-secondary);
}

35
src/routes/home/Home.js Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Home.css';
class Home extends React.Component {
render() {
return (
<div className={s.root}>
<div className={s.formContainer}>
<form method="post" action="/rsvp">
<div className={s.formGroup}>
<label className={s.label} htmlFor="code">
Enter your RSVP code:
</label>
<input
className={s.input}
id="code"
type="text"
name="code"
autoFocus
/>
</div>
<div className={s.formGroup}>
<button className={s.button} type="submit">
RSVP
</button>
</div>
</form>
</div>
</div>
);
}
}
export default withStyles(s)(Home);

15
src/routes/home/index.js Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import Home from './Home';
import Layout from '../../components/Layout';
export default {
path: '/',
action() {
return {
title: 'Welcome',
component: <Layout><Home /></Layout>,
};
},
};

32
src/routes/index.js Normal file
View File

@@ -0,0 +1,32 @@
/* eslint-disable global-require */
// The top-level (parent) route
export default {
path: '/',
// Keep in mind, routes are evaluated in order
children: [
require('./home').default,
require('./rsvp').default,
require('./login').default,
require('./admin').default,
require('./adminPeople').default,
require('./adminPerson').default,
// Wildcard routes, e.g. { path: '*', ... } (must go last)
require('./notFound').default,
],
async action({ next }) {
// Execute each child route until one of them return the result
const route = await next();
// Provide default values for title, description etc.
route.title = `${route.title || 'Untitled Page'} - Daniel and Hana's Wedding`;
route.description = route.description || '';
return route;
},
};

View File

@@ -0,0 +1,51 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.container {
margin: 0 auto;
padding: 0 0 40px;
max-width: 380px;
}
.lead {
font-size: 1.25em;
}
.formGroup {
margin-bottom: 15px;
}
.facebook {
display: block;
box-sizing: border-box;
margin: 0;
padding: 10px 16px;
width: 100%;
outline: 0;
border: 1px solid #3b5998;
border-radius: 0;
background: #3b5998;
color: #fff;
text-align: center;
text-decoration: none;
font-size: 18px;
line-height: 1.3333333;
cursor: pointer;
}
.facebook:hover {
background: #2d4373;
}
.icon {
display: inline-block;
margin: -2px 12px -2px 0;
width: 20px;
height: 20px;
vertical-align: middle;
fill: currentColor;
}

38
src/routes/login/Login.js Normal file
View File

@@ -0,0 +1,38 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Login.css';
class Login extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
};
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.props.title}</h1>
<p className={s.lead}>Log in with your username or company email address.</p>
<div className={s.formGroup}>
<a className={s.facebook} href="/login/facebook">
<svg
className={s.icon}
width="30"
height="30"
viewBox="0 0 30 30"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 16l1-5h-5V7c0-1.544.784-2 3-2h2V0h-4c-4.072 0-7 2.435-7 7v4H7v5h5v14h6V16h4z"
/>
</svg>
<span>Log in with Facebook</span>
</a>
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Login);

17
src/routes/login/index.js Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import Layout from '../../components/Layout';
import Login from './Login';
const title = 'Log In';
export default {
path: '/login',
async action() {
return {
title,
component: <Layout><Login title={title} /></Layout>,
};
},
};

View File

@@ -0,0 +1,12 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.container {
margin: 0 auto;
padding: 0 0 40px;
max-width: var(--max-content-width);
}

View File

@@ -0,0 +1,22 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './NotFound.css';
class NotFound extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
};
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.props.title}</h1>
<p>Sorry, the page you were trying to view does not exist.</p>
</div>
</div>
);
}
}
export default withStyles(s)(NotFound);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import Layout from '../../components/Layout';
import NotFound from './NotFound';
const title = 'Page Not Found';
export default {
path: '*',
action() {
return {
title,
component: <Layout><NotFound title={title} /></Layout>,
status: 404,
};
},
};

24
src/routes/rsvp/Rsvp.css Normal file
View File

@@ -0,0 +1,24 @@
@import '../../components/variables.css';
.root {
padding-left: 20px;
padding-right: 20px;
}
.container {
margin: 0 auto;
padding: 0 0 40px;
max-width: var(--max-content-width);
}
.flexContainer {
display: flex;
flex-direction: row;
flex-flow: row wrap;
}
@media (--sm-viewport) {
.flexContainer {
flex-flow: row nowrap;
}
}

33
src/routes/rsvp/Rsvp.js Normal file
View File

@@ -0,0 +1,33 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Rsvp.css';
import RsvpCard from '../../components/RsvpCard';
class Rsvp extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
people: PropTypes.arrayOf(PropTypes.shape({
firstname: PropTypes.string,
lastname: PropTypes.string,
email: PropTypes.string,
dietary: PropTypes.string,
starter: PropTypes.string,
main: PropTypes.string,
})).isRequired,
};
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.props.title}</h1>
<div className={s.flexContainer}>
{this.props.people.map(person => <RsvpCard key={person.key} person={person} />)}
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Rsvp);

39
src/routes/rsvp/index.js Normal file
View File

@@ -0,0 +1,39 @@
import React from 'react';
import Layout from '../../components/Layout';
import fetchQL from '../../core/fetchQL';
import Rsvp from './Rsvp';
import NotFound from '../notFound/NotFound';
let title = 'RSVP';
export default {
path: '/rsvp/:password',
async action({ params }) {
if (!params.password) {
return { redirect: '/' };
}
const resp = await fetchQL('{getPersonsByPassword{key,firstname,lastname,attending,dietary,starter,main}}', {
password: params.password,
});
const { data } = await resp.json();
const people = data && data.getPersonsByPassword;
if (!people || people.length === 0) {
title = 'RSVP Not Found';
return {
title,
component: <Layout><NotFound title={title} /></Layout>,
status: 404,
};
}
return {
title,
component: <Layout><Rsvp title={title} people={people} /></Layout>,
};
},
};