10 Commits

Author SHA1 Message Date
1b48b3fbc2 Placeholder site pre 2.0 2017-09-26 21:38:26 +01:00
77f2fb6bbe Remove gift list page 2017-08-27 12:24:35 +01:00
11c833d644 Add car park information to ceremony page 2017-07-16 17:35:08 +01:00
07f606b5dc Fix emails not sending due to RSVP end date refactor 2017-04-17 20:44:38 +01:00
2e32b09a5c Migrate RSVP end dates to per user 2017-04-16 17:29:41 +01:00
cdc6614c63 Create 2 RSVP end dates 2017-03-29 21:59:05 +01:00
75740699f1 Add link to gift list site 2017-03-14 16:12:14 +00:00
6d75606134 Create gift list page
Add generic vegetarian option to mains
Add additional content about Offley Place
2017-03-07 22:52:34 +00:00
a66bc1cedc Add less transparent bg to scrolled nav
Reposition the background images
2017-03-06 23:39:09 +00:00
3f068dfcec Fix bug that prevented sending of email
Add api key for export route
2017-03-06 22:33:32 +00:00
135 changed files with 421 additions and 13052 deletions

View File

@@ -1,30 +1,12 @@
FROM node:7.2.1-alpine FROM node:8.4.0-alpine
# Need cURL to report to Rollbar COPY ./ /usr/src
RUN apk --no-cache add curl git WORKDIR /usr/src
# Copy app RUN npm install --silent
COPY ./ /usr/src/app
WORKDIR /usr/src/app
# Install dev + prod dependencies and build release
RUN npm install --no-optional --silent
# Environment variables
ENV DATABASE_PATH db/wedding.db
ENV NODE_ENV production ENV NODE_ENV production
ENV PORT 80 ENV PORT 80
EXPOSE 80 EXPOSE 80
# Run production build
RUN npm run build -- --release --silent
RUN curl https://api.rollbar.com/api/1/deploy/ \
-F access_token=8bf333d044df48c19a6929e4b1613a8e \
-F environment=production \
-F revision=$(git log -n 1 --pretty=format:"%H") \
-F local_username="$(git log -n 1 --pretty=format:"%an")"
WORKDIR /usr/src/app/build
CMD [ "node", "server.js" ] CMD [ "node", "server.js" ]

113
README.md
View File

@@ -1,112 +1 @@
# Wedding RSVP site Interstitial site before 2.0
Forked from [React Starter Kit](https://www.reactstarterkit.com)
development built on top of [Node.js](https://nodejs.org/),
[Express](http://expressjs.com/), [GraphQL](http://graphql.org/) and
[React](https://facebook.github.io/react/), containing modern web development
tools such as [Webpack](http://webpack.github.io/), [Babel](http://babeljs.io/)
and [Browsersync](http://www.browsersync.io/). Helping you to stay productive
following the best practices. A solid starting point for both professionals
and newcomers to the industry.
## Getting Started
### Requirements
* Mac OS X, Windows, or Linux
* [Yarn](https://yarnpkg.com/) package + [Node.js](https://nodejs.org/) v6.5 or newer
* `node-gyp` prerequisites mentioned [here](https://github.com/nodejs/node-gyp)
* Text editor or IDE pre-configured with React/JSX/Flow/ESlint ([learn more](./how-to-configure-text-editors.md))
### Quick Start
#### 1. Run `yarn install`
This will install both run-time project dependencies and developer tools listed
in [package.json](../package.json) file.
#### 2. Run `yarn start`
This command will build the app from the source files (`/src`) into the output
`/build` folder. As soon as the initial build completes, it will start the
Node.js server (`node build/server.js`) and [Browsersync](https://browsersync.io/)
with [HMR](https://webpack.github.io/docs/hot-module-replacement) on top of it.
> [http://localhost:3000/](http://localhost:3000/) — Node.js server (`build/server.js`)<br>
> [http://localhost:3000/graphql](http://localhost:3000/graphql) — GraphQL server and IDE<br>
> [http://localhost:3001/](http://localhost:3001/) — BrowserSync proxy with HMR, React Hot Transform<br>
> [http://localhost:3002/](http://localhost:3002/) — BrowserSync control panel (UI)
Now you can open your web app in a browser, on mobile devices and start
hacking. Whenever you modify any of the source files inside the `/src` folder,
the module bundler ([Webpack](http://webpack.github.io/)) will recompile the
app on the fly and refresh all the connected browsers.
![browsersync](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/brwosersync.jpg)
Note that the `yarn start` command launches the app in `development` mode,
the compiled output files are not optimized and minimized in this case.
You can use `--release` command line argument to check how your app works
in release (production) mode:
```shell
$ yarn start -- --release
```
*NOTE: double dashes are required*
### How to Build, Test, Deploy
If you need just to build the app (without running a dev server), simply run:
```shell
$ yarn run build
```
or, for a production build:
```shell
$ yarn run build -- --release
```
or, for a production docker build:
```shell
$ yarn run build -- --release --docker
```
*NOTE: double dashes are required*
After running this command, the `/build` folder will contain the compiled
version of the app. For example, you can launch Node.js server normally by
running `node build/server.js`.
To check the source code for syntax errors and potential issues run:
```shell
$ yarn run lint
```
To launch unit tests:
```shell
$ yarn run test # Run unit tests with Mocha
$ yarn run test:watch # Launch unit test runner and start watching for changes
```
By default, [Mocha](https://mochajs.org/) test runner is looking for test files
matching the `src/**/*.test.js` pattern. Take a look at `src/components/Layout/Layout.test.js`
as an example.
To deploy the app, run:
```shell
$ yarn run deploy
```
The deployment script `tools/deploy.js` is configured to push the contents of
the `/build` folder to a remote server via Git. You can easily deploy your app
to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/),
or [Heroku](https://www.heroku.com/) this way. Both will execute `yarn install --production`
upon receiving new files from you. Note, you should only deploy the contents
of the `/build` folder to a remote server.

305
package-lock.json generated Normal file
View File

@@ -0,0 +1,305 @@
{
"name": "wedding",
"version": "1.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz",
"integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=",
"requires": {
"mime-types": "2.1.17",
"negotiator": "0.6.1"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
"integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
"integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/express/-/express-4.15.5.tgz",
"integrity": "sha1-ZwI1ypWYiQpa6BcLg9tyK4Qu2Sc=",
"requires": {
"accepts": "1.3.4",
"array-flatten": "1.1.1",
"content-disposition": "0.5.2",
"content-type": "1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "1.1.1",
"encodeurl": "1.0.1",
"escape-html": "1.0.3",
"etag": "1.8.1",
"finalhandler": "1.0.6",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "1.1.2",
"on-finished": "2.3.0",
"parseurl": "1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "1.1.5",
"qs": "6.5.0",
"range-parser": "1.2.0",
"send": "0.15.6",
"serve-static": "1.12.6",
"setprototypeof": "1.0.3",
"statuses": "1.3.1",
"type-is": "1.6.15",
"utils-merge": "1.0.0",
"vary": "1.1.2"
}
},
"finalhandler": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz",
"integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=",
"requires": {
"debug": "2.6.9",
"encodeurl": "1.0.1",
"escape-html": "1.0.3",
"on-finished": "2.3.0",
"parseurl": "1.3.2",
"statuses": "1.3.1",
"unpipe": "1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
"integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
"requires": {
"depd": "1.1.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
"statuses": "1.3.1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz",
"integrity": "sha1-KWrKh4qCGBbluF0KKFqZvP9FgvA="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz",
"integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM="
},
"mime-db": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
},
"mime-types": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
"requires": {
"mime-db": "1.30.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz",
"integrity": "sha1-ccDuOxAt4/IC87ZPYI0XP8uhqRg=",
"requires": {
"forwarded": "0.1.2",
"ipaddr.js": "1.4.0"
}
},
"qs": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz",
"integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg=="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
},
"send": {
"version": "0.15.6",
"resolved": "https://registry.npmjs.org/send/-/send-0.15.6.tgz",
"integrity": "sha1-IPI6nJJbdiq4JwX+L52yUqzkfjQ=",
"requires": {
"debug": "2.6.9",
"depd": "1.1.1",
"destroy": "1.0.4",
"encodeurl": "1.0.1",
"escape-html": "1.0.3",
"etag": "1.8.1",
"fresh": "0.5.2",
"http-errors": "1.6.2",
"mime": "1.3.4",
"ms": "2.0.0",
"on-finished": "2.3.0",
"range-parser": "1.2.0",
"statuses": "1.3.1"
}
},
"serve-static": {
"version": "1.12.6",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.6.tgz",
"integrity": "sha1-uXN3P2NEmTTaVOW+ul4x2fQhFXc=",
"requires": {
"encodeurl": "1.0.1",
"escape-html": "1.0.3",
"parseurl": "1.3.2",
"send": "0.15.6"
}
},
"setprototypeof": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
"integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
},
"statuses": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
},
"type-is": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
"integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
"requires": {
"media-typer": "0.3.0",
"mime-types": "2.1.17"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
"integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

View File

@@ -1,226 +1,15 @@
{ {
"name": "wedding", "name": "wedding",
"version": "1.0.0", "version": "1.5.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=6.5", "node": ">=6.5",
"npm": ">=3.10" "npm": ">=3.10"
}, },
"dependencies": { "dependencies": {
"babel-polyfill": "^6.20.0", "express": "^4.15.5"
"babel-runtime": "^6.20.0",
"bluebird": "^3.4.6",
"body-parser": "^1.15.2",
"classnames": "^2.2.5",
"cookie-parser": "^1.4.3",
"core-js": "^2.4.1",
"express": "^4.14.0",
"express-bouncer": "^0.2.0",
"express-graphql": "^0.6.1",
"express-jwt": "^5.1.0",
"fastclick": "^1.0.6",
"fbjs": "^0.8.6",
"graphql": "^0.8.2",
"history": "^4.5.0",
"isomorphic-style-loader": "^1.1.0",
"jsonwebtoken": "^7.2.1",
"node-fetch": "^1.6.3",
"nodemailer": "^3.1.3",
"normalize.css": "^5.0.0",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"pretty-error": "^2.0.2",
"query-string": "^4.2.3",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-google-maps": "6",
"react-icon-base": "^2.0.4",
"react-icons": "^2.2.3",
"rollbar": "^0.6.3",
"sequelize": "^3.28.0",
"slack-node": "^0.1.8",
"source-map-support": "^0.4.6",
"sqlite3": "^3.1.8",
"universal-router": "^2.0.0",
"whatwg-fetch": "^2.0.1"
},
"devDependencies": {
"assets-webpack-plugin": "^3.5.0",
"autoprefixer": "^6.5.3",
"babel-cli": "^6.18.0",
"babel-core": "^6.18.2",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.8",
"babel-plugin-rewire": "^1.0.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-latest": "^6.16.0",
"babel-preset-react": "^6.16.0",
"babel-preset-react-optimize": "^1.0.1",
"babel-preset-stage-0": "^6.16.0",
"babel-register": "^6.18.0",
"babel-template": "^6.16.0",
"babel-types": "^6.19.0",
"browser-sync": "^2.18.2",
"chai": "^3.5.0",
"chokidar": "^1.6.1",
"css-loader": "^0.26.0",
"editorconfig-tools": "^0.1.1",
"enzyme": "^2.6.0",
"extend": "^3.0.0",
"file-loader": "^0.9.0",
"front-matter": "^2.1.1",
"git-repository": "^0.1.4",
"glob": "^7.1.1",
"json-loader": "^0.5.4",
"lint-staged": "^3.2.2",
"markdown-it": "^8.2.2",
"mkdirp": "^0.5.1",
"mocha": "^3.1.2",
"pixrem": "^3.0.2",
"pleeease-filters": "^3.0.0",
"postcss": "^5.2.5",
"postcss-calc": "^5.3.1",
"postcss-color-function": "^2.0.1",
"postcss-custom-media": "^5.0.1",
"postcss-custom-properties": "^5.0.1",
"postcss-custom-selectors": "^3.0.0",
"postcss-flexbugs-fixes": "^2.0.0",
"postcss-loader": "^1.1.1",
"postcss-media-minmax": "^2.1.2",
"postcss-nested": "^1.0.0",
"postcss-nesting": "^2.3.1",
"postcss-partial-import": "^2.1.0",
"postcss-pseudoelements": "^3.0.0",
"postcss-selector-matches": "^2.0.5",
"postcss-selector-not": "^2.0.0",
"postcss-url": "^5.1.2",
"pre-commit": "^1.2.2",
"raw-loader": "^0.5.1",
"react-addons-test-utils": "15.4.1",
"react-deep-force-update": "^2.0.1",
"react-hot-loader": "^3.0.0-beta.6",
"redbox-react": "^1.3.3",
"rimraf": "^2.5.4",
"sinon": "^2.0.0-pre.3",
"stylefmt": "^5.0.2",
"stylelint": "^7.6.0",
"stylelint-config-standard": "^15.0.0",
"url-loader": "^0.5.7",
"webpack": "^1.13.3",
"webpack-hot-middleware": "^2.13.2",
"webpack-middleware": "^1.5.1"
},
"optionalDependencies": {
"eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0",
"eslint-loader": "^1.6.1",
"eslint-plugin-css-modules": "^1.0.9",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^2.2.3",
"eslint-plugin-react": "^6.7.1"
},
"babel": {
"presets": [
"react",
"stage-0"
],
"plugins": [
"syntax-trailing-function-commas",
"transform-async-to-generator",
"transform-es2015-destructuring",
"transform-es2015-parameters",
"transform-es2015-duplicate-keys",
"transform-es2015-modules-commonjs",
"transform-exponentiation-operator",
"transform-runtime"
],
"env": {
"test": {
"plugins": [
"rewire"
]
}
}
},
"eslintConfig": {
"parser": "babel-eslint",
"extends": [
"airbnb",
"plugin:css-modules/recommended"
],
"plugins": [
"css-modules"
],
"globals": {
"__DEV__": true
},
"env": {
"browser": true
},
"rules": {
"arrow-parens": "off",
"generator-star-spacing": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": "off",
"react/forbid-prop-types": "off",
"react/jsx-filename-extension": "off",
"react/no-danger": "off",
"react/no-unused-prop-types": "off",
"react/prefer-stateless-function": "off"
}
},
"stylelint": {
"extends": "stylelint-config-standard",
"rules": {
"string-quotes": "single",
"property-no-unknown": [
true,
{
"ignoreProperties": [
"composes"
]
}
],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": [
"global"
]
}
]
}
},
"pre-commit": "lint:staged",
"lint-staged": {
"*.{md,sh,txt,xml,html,json}": [
"editorconfig-tools fix",
"git add"
],
"*.{js,jsx}": [
"eslint --fix",
"git add"
],
"*.{css,less,scss,sss}": [
"stylefmt",
"stylelint",
"git add"
]
}, },
"scripts": { "scripts": {
"lint:js": "eslint src tools", "start": "node server.js"
"lint:css": "stylelint \"src/**/*.{css,less,scss,sss}\"",
"lint:staged": "lint-staged",
"lint": "npm run lint:js && npm run lint:css",
"test": "mocha \"src/**/*.test.js\" --require babel-register --require test/setup.js",
"test:watch": "npm run test -- --reporter min --watch",
"clean": "babel-node tools/run clean",
"copy": "babel-node tools/run copy",
"bundle": "babel-node tools/run bundle",
"build": "babel-node tools/run build",
"deploy": "babel-node tools/run deployToAzureWebApps",
"render": "babel-node tools/run render",
"serve": "babel-node tools/run runServer",
"start": "babel-node tools/run start"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Please read: http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx -->
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="tile.png"/>
<square150x150logo src="tile.png"/>
<wide310x150logo src="tile-wide.png"/>
<square310x310logo src="tile.png"/>
</tile>
</msapplication>
</browserconfig>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
</cross-domain-policy>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,15 +0,0 @@
# humanstxt.org/
# The humans responsible & technology colophon
# TEAM
Daniel Head -- Developer/Groom -- @danhead
# THANKS
Hana Becka, for saying yes
# TECHNOLOGY COLOPHON
CSS3, HTML5, JavaScript
React, Flux, SuperAgent

BIN
public/images/four.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

BIN
public/images/one.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

BIN
public/images/three.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

BIN
public/images/two.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 964 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 KiB

102
public/index.html Normal file
View File

@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=false,shrink-to-fit=no">
<meta charset="UTF-8">
<title>Daniel and Hana - Thank you</title>
<style type="text/css">
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: sans;
font-size: 17px;
}
.heading {
position: fixed;
top: 0;
right: 0;
left: 0;
text-align: center;
z-index: 2;
color: #FEDEFD;
background-color: rgba(27, 35, 43, .6);
margin: 0;
padding: .75rem;
font-size: 1.4rem;
}
.subheading {
position: fixed;
right: 0;
bottom: 0;
left: 0;
text-align: center;
z-index: 2;
color: #FEDEFD;
background-color: rgba(27, 35, 43, .6);
margin: 0;
padding: .5rem;
font-size: .8rem;
}
.image {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
filter: blur(2px) brightness(.8);
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
opacity: 0;
}
.image.isTransitioning {
transition: opacity 4s;
}
.image.isActive {
opacity: 1;
}
.image-one { background-image: url('images/one.jpg'); }
.image-two { background-image: url('images/two.jpg'); }
.image-three { background-image: url('images/three.jpg'); }
.image-four { background-image: url('images/four.jpg'); }
</style>
</head>
<body>
<h1 class="heading">Thank you to all who joined us to celebrate our special day.</h1>
<h2 class="subheading">Our full photo album is coming soon.</h2>
<div class="images">
<div class="image image-one"></div>
<div class="image image-two"></div>
<div class="image image-three"></div>
<div class="image image-four"></div>
</div>
</body>
<script type="text/javascript">
var images = document.querySelectorAll('.image');
var active = 0;
var transitionImage = function(n, o) {
images[n].classList.add('isTransitioning');
images[n].classList.add('isActive');
if (typeof o === 'number') {
images[o].classList.add('isTransitioning');
images[o].classList.remove('isActive');
}
setTimeout(function() {
images[n].classList.remove('isTransitioning');
if (typeof o === 'number') {
images[o].classList.remove('isTransitioning');
}
}, 4000);
}
setTimeout(function() {
transitionImage(active);
setInterval(function() {
var newIndex = (active === images.length -1) ? 0 : active + 1;
console.log(newIndex, active);
transitionImage(newIndex, active);
active = newIndex;
}, 8000);
}, 1000);
</script>
</html>

View File

@@ -1,5 +0,0 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

6
server.js Normal file
View File

@@ -0,0 +1,6 @@
const express = require('express');
const app = express();
app.use(express.static('public'));
app.listen(80);
console.log('App listening on port 80');

View File

@@ -1,189 +0,0 @@
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import FastClick from 'fastclick';
import UniversalRouter from 'universal-router';
import queryString from 'query-string';
import { createPath } from 'history/PathUtils';
import history from './core/history';
import App from './components/App';
import { ErrorReporter, deepForceUpdate } from './core/devUtils';
// Global (context) variables that can be easily accessed from any React component
// https://facebook.github.io/react/docs/context.html
const context = {
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
insertCss: (...styles) => {
// eslint-disable-next-line no-underscore-dangle
const removeCss = styles.map(x => x._insertCss());
return () => { removeCss.forEach(f => f()); };
},
};
function updateTag(tagName, keyName, keyValue, attrName, attrValue) {
const node = document.head.querySelector(`${tagName}[${keyName}="${keyValue}"]`);
if (node && node.getAttribute(attrName) === attrValue) return;
// Remove and create a new tag in order to make it work with bookmarks in Safari
if (node) {
node.parentNode.removeChild(node);
}
if (typeof attrValue === 'string') {
const nextNode = document.createElement(tagName);
nextNode.setAttribute(keyName, keyValue);
nextNode.setAttribute(attrName, attrValue);
document.head.appendChild(nextNode);
}
}
function updateMeta(name, content) {
updateTag('meta', 'name', name, 'content', content);
}
function updateCustomMeta(property, content) { // eslint-disable-line no-unused-vars
updateTag('meta', 'property', property, 'content', content);
}
function updateLink(rel, href) { // eslint-disable-line no-unused-vars
updateTag('link', 'rel', rel, 'href', href);
}
// Switch off the native scroll restoration behavior and handle it manually
// https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
const scrollPositionsHistory = {};
if (window.history && 'scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
}
let onRenderComplete = function initialRenderComplete() {
const elem = document.getElementById('css');
if (elem) elem.parentNode.removeChild(elem);
onRenderComplete = function renderComplete(route, location) {
document.title = route.title;
updateMeta('description', route.description);
// Update necessary tags in <head> at runtime here, ie:
// updateMeta('keywords', route.keywords);
// updateCustomMeta('og:url', route.canonicalUrl);
// updateCustomMeta('og:image', route.imageUrl);
// updateLink('canonical', route.canonicalUrl);
// etc.
let scrollX = 0;
let scrollY = 0;
const pos = scrollPositionsHistory[location.key];
if (pos) {
scrollX = pos.scrollX;
scrollY = pos.scrollY;
} else {
const targetHash = location.hash.substr(1);
if (targetHash) {
const target = document.getElementById(targetHash);
if (target) {
scrollY = window.pageYOffset + target.getBoundingClientRect().top;
}
}
}
// Restore the scroll position if it was saved into the state
// or scroll to the given #hash anchor
// or scroll to top of the page
window.scrollTo(scrollX, scrollY);
// Google Analytics tracking. Don't send 'pageview' event after
// the initial rendering, as it was already sent
if (window.ga) {
window.ga('send', 'pageview', createPath(location));
}
};
};
// Make taps on links and buttons work fast on mobiles
FastClick.attach(document.body);
const container = document.getElementById('app');
let appInstance;
let currentLocation = history.location;
let routes = require('./routes').default;
// Re-render the app when window.location changes
async function onLocationChange(location) {
// Remember the latest scroll position for the previous location
scrollPositionsHistory[currentLocation.key] = {
scrollX: window.pageXOffset,
scrollY: window.pageYOffset,
};
// Delete stored scroll position for next page if any
if (history.action === 'PUSH') {
delete scrollPositionsHistory[location.key];
}
currentLocation = location;
try {
// Traverses the list of routes in the order they are defined until
// it finds the first route that matches provided URL path string
// and whose action method returns anything other than `undefined`.
const route = await UniversalRouter.resolve(routes, {
path: location.pathname,
query: queryString.parse(location.search),
});
// Prevent multiple page renders during the routing process
if (currentLocation.key !== location.key) {
return;
}
if (route.redirect) {
history.replace(route.redirect);
return;
}
appInstance = ReactDOM.render(
<App context={context}>{route.component}</App>,
container,
() => onRenderComplete(route, location),
);
} catch (error) {
console.error(error); // eslint-disable-line no-console
// Current url has been changed during navigation process, do nothing
if (currentLocation.key !== location.key) {
return;
}
// Display the error in full-screen for development mode
if (process.env.NODE_ENV !== 'production') {
appInstance = null;
document.title = `Error: ${error.message}`;
ReactDOM.render(<ErrorReporter error={error} />, container);
return;
}
// Avoid broken navigation in production mode by a full page reload on error
window.location.reload();
}
}
// Handle client-side navigation by using HTML5 History API
// For more information visit https://github.com/mjackson/history#readme
history.listen(onLocationChange);
onLocationChange(currentLocation);
// Enable Hot Module Replacement (HMR)
if (module.hot) {
module.hot.accept('./routes', () => {
routes = require('./routes').default; // eslint-disable-line global-require
if (appInstance) {
try {
// Force-update the whole tree, including components that refuse to update
deepForceUpdate(appInstance);
} catch (error) {
appInstance = null;
document.title = `Hot Update Error: ${error.message}`;
ReactDOM.render(<ErrorReporter error={error} />, container);
return;
}
}
onLocationChange(currentLocation);
});
}

View File

@@ -1,52 +0,0 @@
import React, { PropTypes } from 'react';
const ContextType = {
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
insertCss: PropTypes.func.isRequired,
};
/**
* The top-level React component setting context (global) variables
* that can be accessed from all the child components.
*
* https://facebook.github.io/react/docs/context.html
*
* Usage example:
*
* const context = {
* history: createBrowserHistory(),
* store: createStore(),
* };
*
* ReactDOM.render(
* <App context={context}>
* <Layout>
* <LandingPage />
* </Layout>
* </App>,
* container,
* );
*/
class App extends React.PureComponent {
static propTypes = {
context: PropTypes.shape(ContextType).isRequired,
children: PropTypes.element.isRequired,
};
static childContextTypes = ContextType;
getChildContext() {
return this.props.context;
}
render() {
// NOTE: If you need to add or modify header, footer etc. of the app,
// please do that inside the Layout component.
return React.Children.only(this.props.children);
}
}
export default App;

View File

@@ -1,27 +0,0 @@
@import '../variables.css';
.root {
display: block;
box-sizing: border-box;
margin: 0;
padding: 10px 16px;
width: 100%;
opacity: 0.75;
outline: 0;
border: 4px solid #fff;
border-radius: 14px;
background: var(--colour-nearblack);
color: #fff;
text-align: center;
text-decoration: none;
font-size: 18px;
line-height: 1.3333333;
cursor: pointer;
transition: opacity ease-in-out 0.25s, box-shadow ease-in-out 0.25s;
}
.root:hover,
.root:focus {
opacity: 0.9;
box-shadow: 0 0 4px #fff;
}

View File

@@ -1,28 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Button.css';
class Button extends React.Component {
static propTypes = {
to: PropTypes.string,
children: PropTypes.node,
onClick: PropTypes.func,
};
handleClick = (event) => {
if (this.props.onClick) {
this.props.onClick(event);
}
if (this.props.to) {
window.location.href = this.props.to;
}
};
render() {
const { children, ...props } = this.props;
return <button className={s.root} {...props} onClick={this.handleClick}>{children}</button>;
}
}
export default withStyles(s)(Button);

View File

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

View File

@@ -1,47 +0,0 @@
import React, { PropTypes } from 'react';
import { analytics } from '../config';
class Html extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
style: PropTypes.string,
scripts: PropTypes.arrayOf(PropTypes.string.isRequired),
children: PropTypes.string,
};
render() {
const { title, description, style, scripts, children } = this.props;
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Palanquin+Dark|Niconne" />
{style && <style id="css" dangerouslySetInnerHTML={{ __html: style }} />}
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAY-RO-FaBZyAh8FDJneMc6NjMdK2D8WtQ" />
</head>
<body>
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />
{scripts && scripts.map(script => <script key={script} src={script} />)}
{analytics.google.trackingId &&
<script
dangerouslySetInnerHTML={{ __html:
'window.ga=function(){ga.q.push(arguments)};ga.q=[];ga.l=+new Date;' +
`ga('create','${analytics.google.trackingId}','auto');ga('send','pageview')` }}
/>
}
{analytics.google.trackingId &&
<script src="https://www.google-analytics.com/analytics.js" async defer />
}
</body>
</html>
);
}
}
export default Html;

View File

@@ -1,169 +0,0 @@
@import '../../../node_modules/normalize.css/normalize.css';
@import '../variables.css';
/*
* Base styles
* ========================================================================== */
html {
color: #222;
font-weight: 100;
font-size: 1em; /* ~16px; */
font-family: var(--font-family-base);
line-height: 1.375; /* ~22px */
}
a {
color: #0074c2;
}
/*
* Remove text-shadow in selection highlight:
* https://twitter.com/miketaylr/status/12228805301
*
* These selection rule sets have to be separate.
* Customize the background color to match your design.
*/
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
/*
* A better looking default horizontal rule
*/
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #ccc;
margin: 1em 0;
padding: 0;
}
/*
* Remove the gap between audio, canvas, iframes,
* images, videos and the bottom of their containers:
* https://github.com/h5bp/html5-boilerplate/issues/440
*/
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: middle;
}
/*
* Remove default fieldset styles.
*/
fieldset {
border: 0;
margin: 0;
padding: 0;
}
/*
* Allow only vertical resizing of textareas.
*/
textarea {
resize: vertical;
}
/*
* Browser upgrade prompt
* ========================================================================== */
:global(.browserupgrade) {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
/*
* Print styles
* Inlined to avoid the additional HTTP request:
* http://www.phpied.com/delay-loading-your-print-css/
* ========================================================================== */
@media print {
*,
*::before,
*::after {
background: transparent !important;
color: #000 !important; /* Black prints faster: http://www.sanbeiji.com/archives/953 */
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]::after {
content: ' (' attr(href) ')';
}
abbr[title]::after {
content: ' (' attr(title) ')';
}
/*
* Don't show links that are fragment identifiers,
* or use the `javascript:` pseudo protocol
*/
a[href^='#']::after,
a[href^='javascript:']::after {
content: '';
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
/*
* Printing Tables:
* http://css-discuss.incutio.com/wiki/Printing_Tables
*/
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}

View File

@@ -1,22 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Layout.css';
import Navigation from '../Navigation';
class Layout extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,
current: PropTypes.string,
};
render() {
return (
<div>
<Navigation current={this.props.current} />
{this.props.children}
</div>
);
}
}
export default withStyles(s)(Layout);

View File

@@ -1,23 +0,0 @@
/* eslint-env mocha */
/* eslint-disable padded-blocks, no-unused-expressions */
import React from 'react';
import { expect } from 'chai';
import { render } from 'enzyme';
import App from '../App';
import Layout from './Layout';
describe('Layout', () => {
it('renders children correctly', () => {
const wrapper = render(
<App context={{ insertCss: () => {} }}>
<Layout>
<div className="child" />
</Layout>
</App>,
);
expect(wrapper.find('div.child').length).to.eq(1);
});
});

View File

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

View File

@@ -1,42 +0,0 @@
import React, { PropTypes } from 'react';
import history from '../../core/history';
function isLeftClickEvent(event) {
return event.button === 0;
}
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
class Link extends React.Component {
static propTypes = {
to: PropTypes.string.isRequired,
children: PropTypes.node,
onClick: PropTypes.func,
};
handleClick = (event) => {
if (this.props.onClick) {
this.props.onClick(event);
}
if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
return;
}
if (event.defaultPrevented === true) {
return;
}
event.preventDefault();
history.push(this.props.to);
};
render() {
const { to, children, ...props } = this.props;
return <a href={to} {...props} onClick={this.handleClick}>{children}</a>;
}
}
export default Link;

View File

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

View File

@@ -1,21 +0,0 @@
.root {
height: 360px;
width: 100%;
max-width: 480px;
overflow: hidden;
text-shadow: none;
}
.map {
height: 360px;
width: 100%;
max-width: 480px;
}
.info {
color: #000;
}
.infoTitle {
margin: 2px 0 10px;
}

View File

@@ -1,103 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { withGoogleMap, GoogleMap, Marker, InfoWindow } from 'react-google-maps';
import s from './Map.css';
class Map extends React.Component {
static propTypes = {
defaultZoom: PropTypes.number.isRequired,
defaultCenter: PropTypes.shape({
lat: PropTypes.number.isRequired,
lng: PropTypes.number.isRequired,
}).isRequired,
markers: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
position: PropTypes.shape({
lat: PropTypes.number.isRequired,
lng: PropTypes.number.isRequired,
}).isRequired,
showInfo: PropTypes.boolean,
infoContent: PropTypes.string,
})).isRequired,
};
constructor(props) {
super(props);
this.state = {
markers: this.props.markers,
};
}
handleMarkerClick = (targetMarker) => {
this.setState({
markers: this.state.markers.map(marker => {
if (marker === targetMarker) {
return {
...marker,
showInfo: true,
};
}
return marker;
}),
});
};
handleMarkerClose = (targetMarker) => {
this.setState({
markers: this.state.markers.map(marker => {
if (marker === targetMarker) {
return {
...marker,
showInfo: false,
};
}
return marker;
}),
});
}
render() {
const GMap = withGoogleMap(() => (
<GoogleMap
defaultZoom={this.props.defaultZoom}
defaultCenter={this.props.defaultCenter}
>
{this.state.markers.map((marker, index) => (
<Marker
key={index}
defaultAnimation="2"
{...marker}
onClick={() => this.handleMarkerClick(marker)}
>
{marker.showInfo && (
<InfoWindow
onCloseClick={() => this.handleMarkerClose(marker)}
>
<div className={s.info}>
<h6 className={s.infoTitle}>
{marker.url ? (
<a href={marker.url}>{marker.title}</a>
) : marker.title }
</h6>
{marker.infoContent}
</div>
</InfoWindow>
)}
</Marker>
))}
</GoogleMap>
));
return (
<GMap
containerElement={
<div className={s.root} />
}
mapElement={
<div className={s.map} />
}
/>
);
}
}
export default withStyles(s)(Map);

View File

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

View File

@@ -1,37 +0,0 @@
.root {
position: fixed;
top: 0;
right: 0;
left: 0;
height: 40px;
background: rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.container {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
text-align: center;
}
.link {
display: inline-block;
padding: 8px 2% 6px;
text-decoration: none;
font-size: 1.125em;
border-bottom: 2px solid transparent;
transition: color 0.3s ease-in, border-color 0.3s ease-in;
}
.link,
.link:visited {
color: rgba(255, 255, 255, 0.8);
}
.active,
.link:active,
.link:hover {
color: rgba(255, 255, 255, 1);
border-color: #fff;
}

View File

@@ -1,29 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Navigation.css';
import Link from '../Link';
class Navigation extends React.Component {
static propTypes = {
current: PropTypes.string,
};
render() {
if (!this.props.current) {
return null;
}
return (
<div className={s.root} role="navigation">
<div className={s.container}>
<Link className={[s.link, this.props.current === 'home' ? s.active : null].join(' ')} to="/">RSVP</Link>
<Link className={[s.link, this.props.current === 'ceremony' ? s.active : null].join(' ')} to="/church">Church</Link>
<Link className={[s.link, this.props.current === 'reception' ? s.active : null].join(' ')} to="/venue">Venue</Link>
<Link className={[s.link, this.props.current === 'accomodation' ? s.active : null].join(' ')} to="/hotels">Hotels</Link>
<Link className={[s.link, this.props.current === 'transport' ? s.active : null].join(' ')} to="/transport">Transport</Link>
</div>
</div>
);
}
}
export default withStyles(s)(Navigation);

View File

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

View File

@@ -1,12 +0,0 @@
@import '../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

@@ -1,24 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Page.css';
class Page extends React.Component {
static propTypes = {
title: PropTypes.string,
html: PropTypes.string.isRequired,
};
render() {
const { title, html } = this.props;
return (
<div className={s.root}>
<div className={s.container}>
{title && <h1>{title}</h1>}
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
</div>
);
}
}
export default withStyles(s)(Page);

View File

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

View File

@@ -1,102 +0,0 @@
@import '../variables.css';
.root {
margin: 0 auto;
max-width: 420px;
position: relative;
}
.rsvp,
.rsvpForm {
position: absolute;
width: 100%;
transition: opacity ease-in-out 0.25s;
display: none;
opacity: 0;
}
.rsvpVisible,
.rsvpFormVisible {
display: block;
opacity: 1;
}
.formGroup {
margin-bottom: 15px;
}
.label {
display: inline-block;
position: fixed;
top: -1000px;
left: -1000px;
}
.input {
display: block;
box-sizing: border-box;
padding: 12px 16px;
width: 100%;
height: 50px;
opacity: 0.75;
outline: 0;
border: 3px solid #fff;
border-radius: 14px;
background: #fff;
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 {
opacity: 1;
box-shadow: 0 0 4px #fff;
}
.inputInvalid {
animation: shake 0.5s linear;
}
.invalidMsg {
display: none;
padding: 10px 16px;
border: 2px solid var(--colour-red);
border-radius: 14px;
background: var(--colour-nearblack);
color: #fff;
text-decoration: none;
font-size: 18px;
line-height: 1.3333333;
transition: opacity ease-in-out 0.25s, box-shadow ease-in-out 0.25s;
}
.invalidMsgVisible {
display: inline-block;
opacity: 0.9;
}
@keyframes shake {
8%,
41% {
transform: translateX(-10px);
}
25%,
58% {
transform: translateX(10px);
}
75% {
transform: translateX(-5px);
}
92% {
transform: translateX(5px);
}
0%,
100% {
transform: translateX(0);
}
}

View File

@@ -1,152 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './RsvpButton.css';
import fetch from '../../core/fetch';
import Button from '../Button';
class RsvpButton extends React.Component {
static propTypes = {
focusCallback: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {};
}
showRsvpForm = () => {
this.setState({ showRsvpForm: true });
setTimeout(() => {
this.rsvpInput.focus();
}, 500);
}
handleRsvpFocus = () => {
if (this.props.focusCallback) {
this.props.focusCallback(true);
}
this.setState({ inputError: false });
}
handleRsvpBlur = () => {
if (this.props.focusCallback) {
this.props.focusCallback(false);
}
this.setState({ showRsvpForm: false });
}
handleRsvpChange = () => {
this.setState({ inputInvalidMsg: null });
if (this.rsvpInput.value && this.rsvpInput.value.length === 4) {
this.validateRsvp();
}
}
handleFormSubmit = (event) => {
event.preventDefault();
this.validateRsvp();
}
validateRsvp = () => {
fetch(`rsvp/${this.rsvpInput.value}`, {
method: 'get',
credentials: 'include',
}).then(data => {
if (data.status === 200) {
location.href = data.url;
} else if (data.status === 429) {
this.setState({
inputInvalid: true,
inputInvalidMsg: 'You have made too many incorrect attempts. Please try again later.',
inputDisabled: true,
});
setTimeout(() => {
this.setState({
inputInvalid: false,
inputDisabled: false,
});
}, 10 * 60 * 1000);
} else {
this.setState({
inputInvalid: true,
inputInvalidMsg: 'Sorry, that RSVP code is invalid.',
});
setTimeout(() => {
this.setState({
inputInvalid: false,
});
}, 500);
}
}).catch(() => {
this.setState({
inputInvalid: true,
inputInvalidMsg: 'Sorry, there was a problem validating your RSVP code, please try again later.',
});
});
}
render() {
return (
<div className={s.root}>
<div
className={[
s.rsvp,
!this.state.showRsvpForm ? s.rsvpVisible : null,
].join(' ')}
>
<Button onClick={this.showRsvpForm}>
RSVP
</Button>
</div>
<div
className={[
s.rsvpForm,
this.state.showRsvpForm ? s.rsvpFormVisible : null,
].join(' ')}
>
<form
method="post"
action="/rsvp"
ref={(form) => { this.rsvpForm = form; }}
onSubmit={this.handleFormSubmit}
>
<div className={s.formGroup}>
<label className={s.label} htmlFor="code">
Enter your RSVP code
</label>
<input
className={[
s.input,
this.state.inputInvalid ? s.inputInvalid : null,
].join(' ')}
ref={(input) => { this.rsvpInput = input; }}
onFocus={this.handleRsvpFocus}
onBlur={this.handleRsvpBlur}
onChange={this.handleRsvpChange}
disabled={this.state.inputDisabled}
id="code"
type="text"
name="code"
minLength="4"
maxLength="4"
placeholder="Enter your RSVP code"
autoComplete="off"
/>
</div>
</form>
<div
className={[
s.invalidMsg,
this.state.inputInvalidMsg ? s.invalidMsgVisible : null,
].join(' ')}
role="alert"
>
{this.state.inputInvalidMsg}
</div>
</div>
</div>
);
}
}
export default withStyles(s)(RsvpButton);

View File

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

View File

@@ -1,94 +0,0 @@
@import '../variables.css';
.root {
width: 100%;
margin: 0 0 40px;
padding: 20px;
border: 3px solid #fff;
border-radius: 14px;
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
text-align: left;
}
@media (--sm-viewport) {
.root {
width: 40%;
max-width: 400px;
}
}
.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: 12px 16px;
width: 100%;
height: 50px;
opacity: 0.75;
outline: 0;
border: 3px solid #fff;
border-radius: 14px;
background-color: #fff;
color: #616161;
font-size: 18px;
line-height: 1.3333333;
transition: border-color ease-in-out 0.3s, box-shadow ease-in-out 0.3s, background-color ease-in-out 0.3s;
}
.input:disabled {
opacity: 0.5;
}
.input:focus {
opacity: 1;
box-shadow: 0 0 4px #fff;
}
.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 {
opacity: 0;
font-size: 18px;
transition: opacity ease-in-out 0.4s;
}
.rsvpSummarySuccess {
opacity: 1;
}
.rsvpSummaryReadOnly {
opacity: 1;
}

View File

@@ -1,294 +0,0 @@
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 = {
focusCallback: PropTypes.func,
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,
ceremony: PropTypes.boolean,
dietary: PropTypes.string,
starter: PropTypes.string,
main: PropTypes.string,
}).isRequired,
isEditable: PropTypes.bool.isRequired,
};
constructor(props) {
super(props);
this.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,
};
if (!this.props.isEditable) {
this.state.summaryState = 'readonly';
this.state.summary = 'Sorry, you can no longer change your RSVP.';
}
}
isRSVPComplete(prop, value) {
function isOK(str) {
return (str === 'saved' || str === 'success');
}
if (prop === 'attending' && value === 'false') { return true; }
if (!this.props.person.ceremony) { 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({ saveCompleted: true });
this.saveData('completed', true);
}
}
state[stateKey] = data.success ? 'saved' : 'error';
setTimeout(() => {
this.setState(state);
}, 1000);
}).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);
};
handleDietaryChange = (event) => {
const value = event.target.value;
clearTimeout(this.dietaryChangeTimer);
this.dietaryChangeTimer = setTimeout(() => {
this.saveData('dietary', value);
}, 5000);
}
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}
onFocus={this.props.focusCallback}
disabled={!this.props.isEditable}
>
<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}
onFocus={this.props.focusCallback}
disabled={!this.props.isEditable}
>
<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 && this.props.person.ceremony ? 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}
onFocus={this.props.focusCallback}
disabled={!this.props.isEditable}
>
{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}
onFocus={this.props.focusCallback}
disabled={!this.props.isEditable}
>
{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"
autoComplete="off"
defaultValue={this.props.person.dietary}
onChange={this.handleDietaryChange}
onBlur={this.handleDietaryBlur}
onFocus={this.props.focusCallback}
disabled={!this.props.isEditable}
placeholder="None"
/>
<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,
this.state.summaryState === 'readonly' ? s.rsvpSummaryReadOnly : null,
].join(' ')}
>
{this.state.summary}
</div>
</div>
);
}
}
export default withStyles(s)(RsvpCard);

View File

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

View File

@@ -1,35 +0,0 @@
:root {
/*
* Typography
* ======================================================================== */
--font-family-base: 'Palanquin Dark', 'Segoe UI', 'HelveticaNeue-Light', sans-serif;
--font-family-cursive: 'Niconne', cursive;
/*
* Layout
* ======================================================================== */
--max-content-width: 1000px;
/*
* Media queries breakpoints
* ======================================================================== */
@custom-media --xs-viewport (min-width: 480px);
@custom-media --sm-viewport (min-width: 768px);
@custom-media --md-viewport (min-width: 992px);
@custom-media --lg-viewport (min-width: 1200px);
/*
* Colour palette
* ======================================================================== */
--colour-nearblack: #282828;
--colour-primary: #6495ed;
--colour-secondary: #2a6feb;
--colour-tertiary: #2a6feb;
--colour-grey: #ccc;
--colour-green: #87eda2;
--colour-red: #fa7575;
}

View File

@@ -1,6 +0,0 @@
import { adminUsers } from '../config';
export default function (email) {
return adminUsers.indexOf(email) !== -1;
}

View File

@@ -1,13 +0,0 @@
/* eslint-disable global-require */
if (module.hot || process.env.NODE_ENV !== 'production') {
module.exports = {
// The red box (aka red screen of death) component to display your errors
// https://github.com/commissure/redbox-react
ErrorReporter: require('redbox-react').default,
// Force-updates React component tree recursively
// https://github.com/gaearon/react-deep-force-update
deepForceUpdate: require('react-deep-force-update'),
};
}

View File

@@ -1,69 +0,0 @@
import Nodemailer from 'nodemailer';
import { email, starters, mains, getRsvpEnd } from '../config';
import Rollbar from './rollbar';
import Settings from '../data/models/Settings';
const transporter = Nodemailer.createTransport({
service: 'gmail',
auth: {
user: email.senderAddress,
pass: email.password,
},
});
// Debounce sending the email to avoid spamming
const emailTimers = {};
function getEmailText(person) {
return `Dear ${person.firstname}.\n\n
Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.\n
${person.attending ? `Your meal choices are ${starters[person.starter]} and ${mains[person.main]}.\n` : ''}
${person.attending && person.dietary.length > 0 ? `Your dietary requirements are: ${person.dietary}.\n` : ''}
You can modify your RSVP up until ${getRsvpEnd()} here: https://danielandhana.com/${person.password}.\n
${person.attending ? 'See you at our wedding,' : 'Hope to see you soon,'}\n
Daniel and Hana
`;
}
function getEmailHTML(person) {
return `<p>Dear ${person.firstname}.</p>
<p>Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.</p>
${person.attending ? `<p>Your meal choices are <b>${starters[person.starter]}</b> and <b>${mains[person.main]}</b>.</p>` : ''}
${person.attending && person.dietary.length > 0 ? `<p>Your dietary requirements are: <b>${person.dietary}</b>.</p>` : ''}
<p>You can modify your RSVP up until <b>${getRsvpEnd()}</b> <a href="https://danielandhana.com/${person.password}">here</a>.</p>
<p>${person.attending ? 'See you at our wedding,' : 'Hope to see you soon,'}</p>
<p>Daniel and Hana</p>
`;
}
export const sendRSVPEmail = function sendRSVPEmail(person) {
Settings.findAll().then(data => {
if (data[0].email && person.email) {
const mailOptions = {
from: `"${email.senderName}" <${email.senderAddress}>`,
to: person.email,
subject: 'Thank you for your RSVP',
text: getEmailText(person),
html: getEmailHTML(person),
};
// send mail with defined transport object
transporter.sendMail(mailOptions, (error) => {
if (error) {
Rollbar.handleError(error);
}
});
}
}).catch(err => {
Rollbar.handleError(err);
});
};
export const sendRSVPEmailWithDebounce = function sendRSVPEmailWithDebounce(person) {
if (person.key) {
// Stop previous email
clearTimeout(emailTimers[person.key]);
emailTimers[person.key] = setTimeout(() => {
sendRSVPEmail(person);
}, email.debounce);
}
};

View File

@@ -1,6 +0,0 @@
import 'whatwg-fetch';
export default self.fetch.bind(self);
export const Headers = self.Headers;
export const Request = self.Request;
export const Response = self.Response;

View File

@@ -1,24 +0,0 @@
import Promise from 'bluebird';
import fetch, { Request, Headers, Response } from 'node-fetch';
import { host } from '../../config';
fetch.Promise = Promise;
Response.Promise = Promise;
function localUrl(url) {
if (url.startsWith('//')) {
return `https:${url}`;
}
if (url.startsWith('http')) {
return url;
}
return `http://${host}${url}`;
}
function localFetch(url, options) {
return fetch(localUrl(url), options);
}
export { localFetch as default, Request, Headers, Response };

View File

@@ -1,6 +0,0 @@
{
"private": true,
"name": "fetch",
"main": "./fetch.server.js",
"browser": "./fetch.client.js"
}

View File

@@ -1,17 +0,0 @@
import fetch from './fetch';
export default function (query, params) {
return fetch('/graphql', {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
params,
}),
credentials: 'include',
});
}

View File

@@ -1,5 +0,0 @@
import createBrowserHistory from 'history/createBrowserHistory';
// Navigation manager, e.g. history.push('/home')
// https://github.com/mjackson/history
export default process.env.BROWSER && createBrowserHistory();

View File

@@ -1,121 +0,0 @@
/**
* Passport.js reference implementation.
* The database schema used in this sample is available at
* https://github.com/membership/membership.db/tree/master/postgres
*/
import passport from 'passport';
import { Strategy as FacebookStrategy } from 'passport-facebook';
import { User, UserLogin, UserClaim, UserProfile } from '../data/models';
import { auth as config } from '../config';
/**
* Sign in with Facebook.
*/
passport.use(new FacebookStrategy({
clientID: config.facebook.id,
clientSecret: config.facebook.secret,
callbackURL: '/login/facebook/return',
profileFields: ['name', 'email', 'link', 'locale', 'timezone'],
passReqToCallback: true,
}, (req, accessToken, refreshToken, profile, done) => {
/* eslint-disable no-underscore-dangle */
const loginName = 'facebook';
const claimType = 'urn:facebook:access_token';
const fooBar = async () => {
if (req.user) {
const userLogin = await UserLogin.findOne({
attributes: ['name', 'key'],
where: { name: loginName, key: profile.id },
});
if (userLogin) {
// There is already a Facebook account that belongs to you.
// Sign in with that account or delete it, then link it with your current account.
done();
} else {
const user = await User.create({
id: req.user.id,
email: profile._json.email,
logins: [
{ name: loginName, key: profile.id },
],
claims: [
{ type: claimType, value: profile.id },
],
profile: {
displayName: profile.displayName,
gender: profile._json.gender,
picture: `https://graph.facebook.com/${profile.id}/picture?type=large`,
},
}, {
include: [
{ model: UserLogin, as: 'logins' },
{ model: UserClaim, as: 'claims' },
{ model: UserProfile, as: 'profile' },
],
});
done(null, {
id: user.id,
email: user.email,
});
}
} else {
const users = await User.findAll({
attributes: ['id', 'email'],
where: { '$logins.name$': loginName, '$logins.key$': profile.id },
include: [
{
attributes: ['name', 'key'],
model: UserLogin,
as: 'logins',
required: true,
},
],
});
if (users.length) {
const user = users[0];
done(null, {
id: user.id,
email: user.email,
});
} else {
let user = await User.findOne({ where: { email: profile._json.email } });
if (user) {
// There is already an account using this email address. Sign in to
// that account and link it with Facebook manually from Account Settings.
done(null);
} else {
user = await User.create({
email: profile._json.email,
emailConfirmed: true,
logins: [
{ name: loginName, key: profile.id },
],
claims: [
{ type: claimType, value: accessToken },
],
profile: {
displayName: profile.displayName,
gender: profile._json.gender,
picture: `https://graph.facebook.com/${profile.id}/picture?type=large`,
},
}, {
include: [
{ model: UserLogin, as: 'logins' },
{ model: UserClaim, as: 'claims' },
{ model: UserProfile, as: 'profile' },
],
});
done(null, {
id: user.id,
email: user.email,
});
}
}
}
};
fooBar().catch(done);
}));
export default passport;

View File

@@ -1,19 +0,0 @@
import Rollbar from 'rollbar';
import { analytics } from '../config';
const serverToken = analytics.rollbar.serverToken;
export default {
errorHandler() {
return Rollbar.errorHandler(serverToken);
},
handleError(err) {
return Rollbar.handleError(err);
},
init() {
Rollbar.init(serverToken);
Rollbar.handleUncaughtExceptionsAndRejections(serverToken, {
exitOnUncaughtException: true,
});
},
};

View File

@@ -1,38 +0,0 @@
import Slack from 'slack-node';
import { slack as slackCfg } from '../config';
import Rollbar from './rollbar';
import Settings from '../data/models/Settings';
const slack = new Slack();
slack.setWebhook(slackCfg.webhook);
// Debounce sending the slack message to avoid spamming
const slackTimers = {};
export const sendSlackMsg = function sendSlackMsg(text, channel) {
Settings.findAll().then(data => {
if (data[0].slack) {
slack.webhook({
channel,
username: 'weddingbot',
text,
}, err => {
if (err) {
Rollbar.handleError(err);
}
});
}
}).catch(err => {
Rollbar.handleError(err);
});
};
export const sendSlackMsgWithDebounce = function sendSlackMsgWithDebounce(text, channel, key) {
if (key) {
// Stop previous msg
clearTimeout(slackTimers[key]);
slackTimers[key] = setTimeout(() => {
sendSlackMsg(text, channel);
}, slackCfg.debounce);
}
};

View File

@@ -1,5 +0,0 @@
{
"rules": {
"new-cap": 0
}
}

View File

@@ -1,77 +0,0 @@
import DataType from 'sequelize';
import Model from '../sequelize';
const passwordChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'm', 'n', 'p', 'q', 'r', 't', 'u',
'w', 'x', 'y', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'W',
'X', 'Y', '2', '3', '4', '6', '7', '8', '9'];
function generatePassword() {
let pwd = '';
for (let i = 0; i < 4; i += 1) {
pwd += passwordChars[Math.floor(Math.random() * passwordChars.length)];
}
return pwd;
}
const Person = Model.define('Person', {
key: {
type: DataType.UUID,
defaultValue: DataType.UUIDV1,
primaryKey: true,
},
password: {
type: DataType.STRING(4),
defaultValue() {
return generatePassword();
},
},
firstname: {
type: DataType.STRING(50),
},
lastname: {
type: DataType.STRING(50),
},
email: {
type: DataType.STRING(255),
},
completed: {
type: DataType.BOOLEAN,
defaultValue: false,
},
attending: {
type: DataType.BOOLEAN,
},
ceremony: {
type: DataType.BOOLEAN,
},
dietary: {
type: DataType.STRING(255),
},
starter: {
type: DataType.INTEGER,
defaultValue: -1,
},
main: {
type: DataType.INTEGER,
defaultValue: -1,
},
}, {
indexes: [
{ fields: ['email'] },
],
});
export default Person;

View File

@@ -1,23 +0,0 @@
import DataType from 'sequelize';
import Model from '../sequelize';
const Settings = Model.define('Settings', {
key: {
type: DataType.UUID,
defaultValue: DataType.UUIDV1,
primaryKey: true,
},
slack: {
type: DataType.BOOLEAN,
defaultValue: false,
},
email: {
type: DataType.BOOLEAN,
defaultValue: false,
},
});
export default Settings;

View File

@@ -1,30 +0,0 @@
import DataType from 'sequelize';
import Model from '../sequelize';
const User = Model.define('User', {
id: {
type: DataType.UUID,
defaultValue: DataType.UUIDV1,
primaryKey: true,
},
email: {
type: DataType.STRING(255),
validate: { isEmail: true },
},
emailConfirmed: {
type: DataType.BOOLEAN,
defaultValue: false,
},
}, {
indexes: [
{ fields: ['email'] },
],
});
export default User;

View File

@@ -1,16 +0,0 @@
import DataType from 'sequelize';
import Model from '../sequelize';
const UserClaim = Model.define('UserClaim', {
type: {
type: DataType.STRING,
},
value: {
type: DataType.STRING,
},
});
export default UserClaim;

View File

@@ -1,18 +0,0 @@
import DataType from 'sequelize';
import Model from '../sequelize';
const UserLogin = Model.define('UserLogin', {
name: {
type: DataType.STRING(50),
primaryKey: true,
},
key: {
type: DataType.STRING(100),
primaryKey: true,
},
});
export default UserLogin;

View File

@@ -1,33 +0,0 @@
import DataType from 'sequelize';
import Model from '../sequelize';
const UserProfile = Model.define('UserProfile', {
userId: {
type: DataType.UUID,
primaryKey: true,
},
displayName: {
type: DataType.STRING(100),
},
picture: {
type: DataType.STRING(255),
},
gender: {
type: DataType.STRING(50),
},
location: {
type: DataType.STRING(100),
},
website: {
type: DataType.STRING(255),
},
});
export default UserProfile;

View File

@@ -1,34 +0,0 @@
import sequelize from '../sequelize';
import User from './User';
import UserLogin from './UserLogin';
import UserClaim from './UserClaim';
import UserProfile from './UserProfile';
import Person from './Person';
User.hasMany(UserLogin, {
foreignKey: 'userId',
as: 'logins',
onUpdate: 'cascade',
onDelete: 'cascade',
});
User.hasMany(UserClaim, {
foreignKey: 'userId',
as: 'claims',
onUpdate: 'cascade',
onDelete: 'cascade',
});
User.hasOne(UserProfile, {
foreignKey: 'userId',
as: 'profile',
onUpdate: 'cascade',
onDelete: 'cascade',
});
function sync(...args) {
return sequelize.sync(...args);
}
export default { sync };
export { User, UserLogin, UserClaim, UserProfile, Person };

View File

@@ -1,25 +0,0 @@
import { GraphQLList as List } from 'graphql';
import PersonItemType from '../types/PersonItemType';
import Person from '../models/Person';
import admin from '../../core/admin';
const person = {
type: new List(PersonItemType),
resolve({ request }) {
const params = request.body.params;
const user = request.user || {};
const isAdmin = admin(user.email);
if (params && params.code) {
return Person.findAll({
where: {
password: params.code,
},
});
}
return isAdmin && Person.findAll();
},
};
export default person;

View File

@@ -1,22 +0,0 @@
import { GraphQLList as List } from 'graphql';
import PersonItemType from '../types/PersonItemType';
import Person from '../models/Person';
const getPersonByKey = {
type: new List(PersonItemType),
resolve({ request }) {
const params = request.body.params;
if (params && params.key) {
return Person.findAll({
where: {
key: params.key,
},
limit: 1,
});
}
return false;
},
};
export default getPersonByKey;

View File

@@ -1,22 +0,0 @@
import { GraphQLList as List } from 'graphql';
import PersonItemType from '../types/PersonItemType';
import Person from '../models/Person';
const getPersonsByPassword = {
type: new List(PersonItemType),
resolve({ request }) {
const params = request.body.params;
if (params && params.password) {
return Person.findAll({
where: {
password: params.password,
},
});
}
return false;
},
};
export default getPersonsByPassword;

View File

@@ -1,13 +0,0 @@
import UserType from '../types/UserType';
const me = {
type: UserType,
resolve({ request }) {
return request.user && {
id: request.user.id,
email: request.user.email,
};
},
};
export default me;

View File

@@ -1,12 +0,0 @@
import { GraphQLList as List } from 'graphql';
import SettingsItemType from '../types/SettingsItemType';
import Settings from '../models/Settings';
const settings = {
type: new List(SettingsItemType),
resolve() {
return Settings.findAll();
},
};
export default settings;

View File

@@ -1,25 +0,0 @@
import {
GraphQLSchema as Schema,
GraphQLObjectType as ObjectType,
} from 'graphql';
import me from './queries/me';
import getAllPersons from './queries/getAllPersons';
import getPersonByKey from './queries/getPersonByKey';
import getPersonsByPassword from './queries/getPersonsByPassword';
import settings from './queries/settings';
const schema = new Schema({
query: new ObjectType({
name: 'Query',
fields: {
me,
getAllPersons,
getPersonByKey,
getPersonsByPassword,
settings,
},
}),
});
export default schema;

View File

@@ -1,12 +0,0 @@
import Sequelize from 'sequelize';
import { databasePath } from '../config';
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: databasePath,
define: {
freezeTableName: true,
},
});
export default sequelize;

View File

@@ -1,26 +0,0 @@
import {
GraphQLObjectType as ObjectType,
GraphQLID as ID,
GraphQLString as StringType,
GraphQLBoolean as BooleanType,
GraphQLNonNull as NonNull,
} from 'graphql';
const PersonItemType = new ObjectType({
name: 'PersonItem',
fields: {
key: { type: new NonNull(ID) },
password: { type: StringType },
firstname: { type: StringType },
lastname: { type: StringType },
email: { type: StringType },
completed: { type: BooleanType },
attending: { type: BooleanType },
ceremony: { type: BooleanType },
dietary: { type: StringType },
starter: { type: StringType },
main: { type: StringType },
},
});
export default PersonItemType;

View File

@@ -1,17 +0,0 @@
import {
GraphQLObjectType as ObjectType,
GraphQLID as ID,
GraphQLBoolean as BooleanType,
GraphQLNonNull as NonNull,
} from 'graphql';
const SettingsItemType = new ObjectType({
name: 'SettingsItem',
fields: {
key: { type: new NonNull(ID) },
slack: { type: BooleanType },
email: { type: BooleanType },
},
});
export default SettingsItemType;

View File

@@ -1,16 +0,0 @@
import {
GraphQLObjectType as ObjectType,
GraphQLID as ID,
GraphQLString as StringType,
GraphQLNonNull as NonNull,
} from 'graphql';
const UserType = new ObjectType({
name: 'User',
fields: {
id: { type: new NonNull(ID) },
email: { type: StringType },
},
});
export default UserType;

View File

@@ -1,54 +0,0 @@
@import '../../components/variables.css';
.root {
width: 100vw;
height: 100vh;
}
.bgImage {
position: fixed;
width: 100vw;
height: 100vh;
background: url('/img/offley_2560.jpg');
background-size: cover;
background-position-x: 54%;
background-color: #c1c1c1;
filter: grayscale(1) brightness(0.4);
z-index: -1;
transition: filter ease-in-out 0.5s;
}
.container {
padding-top: 50px;
margin: 0 20px;
}
.headings {
max-width: 640px;
margin: 0 auto 40px;
text-align: center;
color: #fff;
text-shadow: 3px 3px 8px var(--colour-nearblack);
font-size: 1.3em;
}
.headings h1 {
margin: 0 0 10px;
}
.content {
max-width: 960px;
margin: 0 auto;
color: #fff;
text-shadow: 1px 1px 4px var(--colour-nearblack);
}
.hotelLink {
color: rgba(255, 255, 255, 0.8);
transition: color 0.3s ease-in;
}
.hotelLink:hover,
.hotelLink:focus {
color: rgba(255, 255, 255, 1);
}

View File

@@ -1,84 +0,0 @@
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Accomodation.css';
import Map from '../../components/Map';
const offley = {
title: 'Offley Place',
url: 'http://www.offleyplace.com/',
position: {
lat: 51.9282,
lng: -0.3332,
},
showInfo: true,
};
const hotels = [
{
title: 'Hilton Garden Inn',
url: 'http://hiltongardeninn3.hilton.com/en/hotels/united-kingdom/hilton-garden-inn-luton-north-LTNNOGI/index.html',
position: {
lat: 51.9116,
lng: -0.3883,
},
},
{
title: 'Premier Inn',
url: 'http://www.premierinn.com/gb/en/hotels/england/hertfordshire/hitchin/hitchin-town-centre.html',
position: {
lat: 51.9493,
lng: -0.2762,
},
},
{
title: 'Lodge Farm House',
url: 'http://lodgefarmbandb.co.uk',
position: {
lat: 51.9054,
lng: -0.3354,
},
},
{
title: 'Red Lion Inn',
url: 'http://www.theredliongreatoffley.co.uk',
position: {
lat: 51.9266,
lng: -0.3348,
},
},
];
class Accomodation extends React.Component {
constructor(props) {
super(props);
this.markers = [offley].concat(hotels);
}
render() {
return (
<div className={s.root}>
<div className={s.bgImage} />
<div className={s.container}>
<div className={s.headings}>
<h1>Local hotels</h1>
</div>
<div className={s.content}>
<h3>The following hotels are near Offley Place:</h3>
<ul>
{hotels.map((hotel, index) => (
<li key={index}><a className={s.hotelLink} href={hotel.url}>{hotel.title}</a></li>
))}
</ul>
<Map
defaultZoom={12}
defaultCenter={offley.position}
markers={this.markers}
/>
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Accomodation);

View File

@@ -1,15 +0,0 @@
import React from 'react';
import Accomodation from './Accomodation';
import Layout from '../../components/Layout';
export default {
path: '/hotels',
action() {
return {
title: 'Hotels',
component: <Layout current="accomodation"><Accomodation /></Layout>,
};
},
};

View File

@@ -1,28 +0,0 @@
@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 {
margin-left: 10px;
}

View File

@@ -1,106 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Admin.css';
import Link from '../../components/Link';
import Button from '../../components/Button';
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,
starters: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
total: PropTypes.integer,
})).isRequired,
mains: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
total: PropTypes.integer,
})).isRequired,
build: PropTypes.shape({
hash: PropTypes.string,
time: PropTypes.string,
}),
settings: PropTypes.shape({
slack: PropTypes.boolean,
email: PropTypes.boolean,
}),
};
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>
<h2>Food</h2>
<h3>Starters</h3>
<ul>
{this.props.starters.map((starter, index) => (
<li key={index}>{starter.name} - {starter.total}</li>
))}
</ul>
<h3>Mains</h3>
<ul>
{this.props.mains.map((main, index) => (
<li key={index}>{main.name} - {main.total}</li>
))}
</ul>
</div>
<div>
<h3>Settings</h3>
<form method="post" action="/admin/settings">
<div className={s.formGroup}>
<label className={s.label} htmlFor="slack">
Send slack messages on RSVP:
</label>
<input
className={s.input}
type="checkbox"
name="slack"
defaultChecked={this.props.settings.slack}
/>
</div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="email">
Send confirmation emails on RSVP:
</label>
<input
className={s.input}
type="checkbox"
name="email"
defaultChecked={this.props.settings.email}
/>
</div>
<div className={s.formGroup}>
<Button type="submit">
Save
</Button>
</div>
</form>
</div>
<div>
Build hash: <a href={`https://bitbucket.org/dan_head/wedding/commits/${this.props.build.hash}`}>{this.props.build.hash}</a>
</div>
<div>
Build time: {this.props.build.time}
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Admin);

View File

@@ -1,67 +0,0 @@
import React from 'react';
import Layout from '../../components/Layout';
import fetchQL from '../../core/fetchQL';
import fetch from '../../core/fetch';
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 configResp = await fetch('/config.json', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
});
const settingsResp = await fetchQL('{settings{slack,email}}');
const { data } = await resp.json();
const config = await configResp.json();
let settings = await settingsResp.json();
if (settings && settings.data && settings.data.settings && settings.data.settings[0]) {
settings = settings.data.settings[0];
} else {
settings = {
slack: false,
email: false,
};
}
const people = data.getAllPersons || [];
const food = {
starters: starters.map((name, index) => {
const total = people.filter(person => person.starter === `${index}`).length;
return { name, total };
}),
mains: mains.map((name, index) => {
const total = people.filter(person => person.main === `${index}`).length;
return { name, total };
}),
};
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}
starters={food.starters}
mains={food.mains}
build={config.build}
settings={settings}
/></Layout>,
};
},
};

View File

@@ -1,83 +0,0 @@
@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);
}
table {
border-spacing: 0;
}
td {
position: relative;
padding: 0 14px;
}
.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);
}
.personForm {
display: none;
}
.personFormVisible {
display: block;
}
.sortIcon {
display: none;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.showSortIcon {
display: inline-block;
}
.importForm {
display: none;
}
.importFormVisible {
display: block;
}

View File

@@ -1,295 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdminPeople.css';
import Link from '../../components/Link';
import Button from '../../components/Button';
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,
ceremony: PropTypes.boolean,
email: PropTypes.string,
password: PropTypes.string,
attending: PropTypes.boolean,
})).isRequired,
};
constructor(props) {
super(props);
this.state = {
people: this.props.people,
personFormVisible: false,
sortDirection: false,
};
}
sortPeople = (event, sortedBy) => {
event.preventDefault();
const sortDirection = (sortedBy === this.state.sortedBy) ? !this.state.sortDirection : true;
this.setState({
sortedBy,
sortDirection,
people: this.props.people.sort((a, b) => {
if (a[sortedBy] > b[sortedBy]) {
return sortDirection ? 1 : -1;
}
return sortDirection ? -1 : 1;
}),
});
}
togglePersonForm = (event) => {
event.preventDefault();
this.setState({ personFormVisible: !this.state.personFormVisible });
}
openImportForm = (event) => {
event.preventDefault();
this.setState({ openImportForm: true });
}
render() {
return (
<div className={s.root}>
<div className={s.container}>
<h1>{this.props.title}</h1>
{this.state.people.length === 0 ? <p> No people yet ☹️</p> :
<table>
<thead>
<tr>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'firstname' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'firstname' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'firstname')}>First name</a>
</td>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'lastname' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'lastname' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'lastname')}>Last name</a>
</td>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'email' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'email' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'email')}>Email</a>
</td>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'password' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'password' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'password')}>Password</a>
</td>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'ceremony' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'ceremony' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'ceremony')}>Ceremony</a>
</td>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'completed' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'completed' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'completed')}>Responded</a>
</td>
</tr>
</thead>
<tbody>
{this.state.people.map(person => (
<tr key={person.key}>
<td>{person.firstname}</td>
<td>{person.lastname}</td>
<td>{person.email}</td>
<td><Link to={`/rsvp/${person.password}`}>{person.password}</Link></td>
<td>{person.ceremony ? 'Full day' : 'Evening only'}</td>
<td>{person.completed ? 'Yes' : 'No'}</td>
<td>
<Button to={`/admin/person/${person.key}`}>Edit</Button>
</td>
<td>
<form method="post" action="/admin/person/delete">
<input type="hidden" name="key" value={person.key} />
<Button type="submit">
Delete
</Button>
</form>
</td>
</tr>
))}
</tbody>
</table>
}
<h2>
<a href="#openpersonform" onClick={this.togglePersonForm}>
<span>{this.state.personFormVisible ? '-' : '+'}</span> Add Person
</a>
</h2>
<form
method="post"
action="/admin/person"
className={[
s.personForm,
this.state.personFormVisible ? s.personFormVisible : null,
].join(' ')}
>
<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="ceremony">
Invite type:
</label>
<select
className={s.input}
id="cermony"
name="ceremony"
>
<option value="true">Full day</option>
<option value="false">Evening only</option>
</select>
</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 type="submit">
Save
</Button>
</div>
</form>
<div>
<a href="#openimportform" onClick={this.openImportForm}>Import data</a>
{!this.state.openImportForm ? ' / ' : null}
{!this.state.openImportForm ? <a href="/admin/people/export">Export data</a> : null}
</div>
<form
method="post"
action="/admin/people/import"
className={[
s.importForm,
this.state.openImportForm ? s.importFormVisible : null,
].join(' ')}
>
<div className={s.formGroup}>
<label className={s.label} htmlFor="importdata">
Enter JSON:
</label>
<textarea
className={s.input}
id="importdata"
type="text"
name="importdata"
/>
</div>
<div className={s.formGroup}>
<Button type="submit">
Save
</Button>
</div>
</form>
</div>
</div>
);
}
}
export default withStyles(s)(AdminPeople);

View File

@@ -1,32 +0,0 @@
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,ceremony,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

@@ -1,49 +0,0 @@
@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);
}

View File

@@ -1,121 +0,0 @@
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdminPerson.css';
import Button from '../../components/Button';
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,
ceremony: 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">
<input
type="hidden"
name="key"
value={this.props.person.key}
/>
<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="ceremony">
Invite type:
</label>
<select
className={s.input}
id="cermony"
name="ceremony"
defaultValue={this.props.person.ceremony ? 'true' : 'false'}
>
<option value="true">Full day</option>
<option value="false">Evening only</option>
</select>
</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 type="submit">
Save
</Button>
</div>
</form>
</div>
</div>
);
}
}
export default withStyles(s)(AdminPerson);

View File

@@ -1,38 +0,0 @@
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,ceremony,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

@@ -1,48 +0,0 @@
@import '../../components/variables.css';
.root {
width: 100vw;
height: 100vh;
}
.bgImage {
position: fixed;
width: 100vw;
height: 100vh;
background: url('/img/church_2560.jpg');
background-size: cover;
background-position-x: 54%;
background-color: #c1c1c1;
filter: grayscale(1) brightness(0.4);
z-index: -1;
transition: filter ease-in-out 0.5s;
}
.container {
padding-top: 50px;
margin: 0 20px;
}
.headings {
max-width: 640px;
margin: 0 auto 40px;
text-align: center;
color: #fff;
text-shadow: 3px 3px 8px var(--colour-nearblack);
font-size: 1.3em;
}
.headings h1 {
margin: 0 0 10px;
}
.subheading {
font-family: var(--font-family-cursive);
}
.content {
max-width: 960px;
margin: 0 auto;
color: #fff;
text-shadow: 1px 1px 4px var(--colour-nearblack);
}

View File

@@ -1,51 +0,0 @@
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Ceremony.css';
import Map from '../../components/Map';
const church = {
title: 'All Saints\' Church',
position: {
lat: 51.7109,
lng: -0.4479,
},
showInfo: true,
};
class Ceremony extends React.Component {
render() {
return (
<div className={s.root}>
<div
className={s.bgImage}
/>
<div className={s.container}>
<div className={s.headings}>
<h1>All Saints&#8217; Church</h1>
<h2 className={s.subheading}>Church Lane<br />Kings Langley<br />WD4 8JS</h2>
</div>
<div className={s.content}>
<h3>When</h3>
<p>The ceremony begins at 1pm</p>
<h3>Where</h3>
<p>The church is located at the southern end of Kings Langley high street.
Parking is available across the road at the Rose and Crown pub.</p>
<Map
defaultZoom={14}
defaultCenter={church.position}
markers={[church]}
/>
<h3>About the church</h3>
<p>All Saints Church is a medieval church with parts dating from the 13th century.
The main features of the church are the beautiful reredos, depicting
The Last Supper, and the fine Jacobean pulpit with its intricate carvings.
The Tower has a peal of 8 bells that are regularly rung before Services.</p>
<p>The church was chosen because it is Dan&#8217;s family church.</p>
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Ceremony);

View File

@@ -1,15 +0,0 @@
import React from 'react';
import Ceremony from './Ceremony';
import Layout from '../../components/Layout';
export default {
path: '/church',
action() {
return {
title: 'All Saints\' Church',
component: <Layout current="ceremony"><Ceremony /></Layout>,
};
},
};

View File

@@ -1,47 +0,0 @@
* {
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

@@ -1,32 +0,0 @@
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);

View File

@@ -1,17 +0,0 @@
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,
};
},
};

View File

@@ -1,54 +0,0 @@
@import '../../components/variables.css';
.root {
width: 100vw;
height: 100vh;
}
.bgImage {
position: fixed;
width: 100vw;
height: 100vh;
background: url('/img/prague_2560.jpg');
background-size: cover;
background-position-x: 54%;
background-color: #c1c1c1;
filter: grayscale(1) brightness(0.6);
z-index: -1;
transition: filter ease-in-out 0.5s;
}
.bgImageFocus {
filter: grayscale(0.4) brightness(1);
}
.container {
padding-top: 50px;
margin: 0 20px;
}
.headings {
max-width: 640px;
margin: 0 auto 40px;
text-align: center;
color: #fff;
text-shadow: 3px 3px 8px var(--colour-nearblack);
font-size: 1.3em;
}
.headings h1 {
margin: 0 0 10px;
}
.headings h2 {
margin: 0 0 60px;
}
.headings h3 {
margin: 0 0 20px;
font-family: var(--font-family-cursive);
}
.rsvp {
padding-top: 10vh;
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Home.css';
import RsvpButton from '../../components/RsvpButton';
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
focusBgImage = (focus) => {
this.setState({ focusBgImage: focus });
}
render() {
return (
<div className={s.root}>
<div
className={[
s.bgImage,
this.state.focusBgImage ? s.bgImageFocus : null,
].join(' ')}
/>
<div className={s.container}>
<div className={s.headings}>
<h1>Daniel and Hana</h1>
<h2>are getting married</h2>
<h3>- 27th July 2017 -</h3>
</div>
<div className={s.rsvp}>
<RsvpButton
focusCallback={this.focusBgImage}
/>
</div>
</div>
</div>
);
}
}
export default withStyles(s)(Home);

View File

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

Some files were not shown because too many files have changed in this diff Show More