🔥
This commit is contained in:
13
src/routes/admin/Admin.css
Normal file
13
src/routes/admin/Admin.css
Normal 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
36
src/routes/admin/Admin.js
Normal 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
41
src/routes/admin/index.js
Normal 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>,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
77
src/routes/adminPeople/AdminPeople.css
Normal file
77
src/routes/adminPeople/AdminPeople.css
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
112
src/routes/adminPeople/AdminPeople.js
Normal file
112
src/routes/adminPeople/AdminPeople.js
Normal 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);
|
||||
32
src/routes/adminPeople/index.js
Normal file
32
src/routes/adminPeople/index.js
Normal 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>,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
77
src/routes/adminPerson/AdminPerson.css
Normal file
77
src/routes/adminPerson/AdminPerson.css
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
101
src/routes/adminPerson/AdminPerson.js
Normal file
101
src/routes/adminPerson/AdminPerson.js
Normal 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);
|
||||
38
src/routes/adminPerson/index.js
Normal file
38
src/routes/adminPerson/index.js
Normal 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>,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
47
src/routes/error/ErrorPage.css
Normal file
47
src/routes/error/ErrorPage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/routes/error/ErrorPage.js
Normal file
32
src/routes/error/ErrorPage.js
Normal 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
17
src/routes/error/index.js
Normal 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
73
src/routes/home/Home.css
Normal 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
35
src/routes/home/Home.js
Normal 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
15
src/routes/home/index.js
Normal 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
32
src/routes/index.js
Normal 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;
|
||||
},
|
||||
|
||||
};
|
||||
51
src/routes/login/Login.css
Normal file
51
src/routes/login/Login.css
Normal 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
38
src/routes/login/Login.js
Normal 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
17
src/routes/login/index.js
Normal 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>,
|
||||
};
|
||||
},
|
||||
};
|
||||
12
src/routes/notFound/NotFound.css
Normal file
12
src/routes/notFound/NotFound.css
Normal 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);
|
||||
}
|
||||
22
src/routes/notFound/NotFound.js
Normal file
22
src/routes/notFound/NotFound.js
Normal 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);
|
||||
19
src/routes/notFound/index.js
Normal file
19
src/routes/notFound/index.js
Normal 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
24
src/routes/rsvp/Rsvp.css
Normal 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
33
src/routes/rsvp/Rsvp.js
Normal 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
39
src/routes/rsvp/index.js
Normal 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>,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user