diff --git a/.gitignore b/.gitignore index 9d67d47..c79cab9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /node_modules +.env /dist +/static/images-local /static/bundle.js /static/bundle.js.map .DS_Store diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..6f86b16 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node server.js \ No newline at end of file diff --git a/README.md b/README.md index 9530e57..dd746cf 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,76 @@ -# Delivereat +![Screenshot](./static/images/lovely-grubbly-screen-shots.png) -In this project we will create a database backed version of Delivereat. We will build it as a new project, but feel free to use your original Delivereat project as reference. Be aware that because the data structures we will be working with here are likely to be different, it may not be possible to directly re-use your UI components. +# Lovely Grubbly -A few notes before we get started +## Order takeaway food online for delivery +**Lovely Grubbly** is an online restaurant which brings the best local food to your doorstep. It allows you to browse the menu and add dishes to the shopping basket which shows a full breakdown of costs including dellivery. To place your order you enter your details and immediatly receive confirmation by text with an order ID. -* Fork and clone this repo -* Start by building the simplest thing that works. Add to it as you go along. Test your application as frequently as possible to make sure it does what you expect -* Commit frequently and push to Github -* Implement features one at a time -* Make sure your app is responsive -* You may want to design the API first, before implementing the UI that uses and API +> [View the live demo](https://lovely-grubbly.herokuapp.com) -## Features +> [View the repo on Github](https://github.com/rolandjlevy/lovely-grubbly) -**Database and API** +--- -* Design a database that will allow you to store a menu and orders. Start out with pen and paper first sketching out the tables and columns you will need, as well as the relationships between the tables. -* The database will need to store a menu which will contain item name and price. We will also need to store orders. Each order can have multiple menu items and each menu item can appear in multiple orders. Each menu item ordered will also need to have a quantity. -* Store the SQL commands used to create database and populate with initial data in a `database.sql` file in your repo. It will allow us to review your database code and also make it easy for you to rebuild database. -* Create a RESTful API that will allow you to get the menu and save new orders -* Test the API using Postman - -**Menu** -* Design and build a front end UI that will load the menu from the API and display it to the user - -**Order** - -* Update the menu page to make it an order page -* It should allow the user to specify quantities of items to order -* It should add a delivery charge and display the total order cost -* Create functionality to submit orders to the API and display a notification to the user with order id - -## Stretch goals - -**Own feature** - -* Design and implement a feature of your choosing - -**SMS notification** - -* Add a phone number input to your UI and a column in orders table to store it. -* Update the API to receive the phone number as part of the order -* Sign up for an account with Twilio. It's an API that allows you to send SMS messages and do lots of other cool things with phones. Use the signup code WELOVECODE to receive $20 credit. -* Implement SMS notification using Twilio to send an SMS notification to a user letting them know that the order has been received. - -**Unit tests** - -* Add unit tests to your application where appropriate - -## Technical notes - -* Run `npm install` after cloning to download all dependencies -* Use `npm run dev -- --watch` to build React -* You will need to create a `.env` file to store your database credentials. Make sure you add it to `.gitignore` file so that the credentials do not get commit to git and end up in public. -* Use `node server.js` to run the Node server in another tab -* Place all static files such as images and CSS in the `static` folder. When requesting those files from the server use `/static` at the beginning of the URL. For example `` -* `bundle.js` produced by webpack will be output in the `static` folder -* To send data server using a POST, PUT or PATCH request you can do something the example below, where `order` is an object we want to send to the server and `http://localhost:8080/api/order` is URL we want to send it to. - -```js -fetch('http://localhost:8080/api/order', { - method: 'post', - body: JSON.stringify(order), - headers: { - 'Content-Type': 'application/json' - } - }).then(function(response) { - return response.json(); - }).then(data => { - // handle response - }); +## Installation and set up ++ Clone the project and run `npm install` ++ Create your own local PostreSQL database instance and create the tables by running `pgweb` navigating to localhost:8081 and running the query in the `database.sql` file. ++ Create a `.env` file with the following variables +``` +DB_HOST=localhost +DB_NAME= +DB_USERNAME= +DB_PASSWORD= +TWILIO_SID_TEST= +TWILIO_AUTH_TEST= +TWILIO_SID_LIVE= +TWILIO_AUTH_LIVE= ``` -* Check out [Nodemon](https://nodemon.io/) to automatically rebuild and restart your server when changes are saved. - -## README - -* Produce a README.md which explains - * what the project does - * what technologies it uses - * how to build it and run it - * any unresolved issues the user should be aware of ++ Run `npm start` to launch the app and navigate to localhost:8080 ++ Use `npm run dev -- --watch` to build React ++ Navigate to `localhost:8080` in your browser to view + +### API Keys needed ++ [Twilio API](https://www.twilio.com/docs/libraries/node) - for sending booking confirmation by text + +--- + +### Tech stack ++ React ++ PostgreSQL ++ Node.js ++ Express ++ Handlebars ++ SCSS ++ Classnames ++ Flex-box ++ Git + +### Build tools +- Webpack +- Babel + +### Stages of development ++ Planned structure of the database tables and their relationships ++ Created database tables, sourced text and image data, entered data into database tables ++ Set up and created the core functionality in React ++ Used SCSS and Flexbox to style the UI ++ Implemented functionality for: +> + menu listing page +> + shopping basket +> + viewing an order + +### Functionality and features ++ Food courses, Startes / Mains / Desserts can be filtered ++ Menu items can be added, removed from the basket and increase/decreased in quantity ++ Added menu items can be viewed in the basket menu with a breakdown of costs ++ Each item has an image icon which is a tooltip showing a preview of the menu item ++ A booking can then be make by adding your name and number ++ You receive a booking confirmation by text with your booking ID ++ You can view your order by clicking on the member icon entering your order ID + +### Desired features with more time ++ To have a responsive layout for tablet and desktop screen sizes ++ To calculate the estimated time of delivery based on the customer's location ++ To enable customers to track their order on their phone ++ Allow customers to amend their order within a limited time period diff --git a/basket-layout.txt b/basket-layout.txt new file mode 100644 index 0000000..5cb4668 --- /dev/null +++ b/basket-layout.txt @@ -0,0 +1,16 @@ +Your Basket +============== + +- 1 + Grilled Mexican Chicken +- 1 + Vegetarian - Tastiest Grilled Fajita +- 1 + Veggies to Ever Grace Your Burrito Meal +- 1 + Southwest, BBQ, Diet Coke, Sprite, Guacamole + +Subtotal £39.99 +Delivery Fee £2.99 +Total £42.97 + +Name: ____________ +Tel: ____________ + +[ Place Order ] \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..1b9698f --- /dev/null +++ b/database.sql @@ -0,0 +1,51 @@ +CREATE database cafe + +DROP TABLE menu_purchase + +-- create tables + +CREATE table menu ( + id serial PRIMARY KEY, + item text NOT NULL, + price numeric(4,2) NOT NULL, + img text NOT NULL, + course text NOT NULL +); + +-- DROP TABLE menu_purchase +-- DROP TABLE purchase + +CREATE table purchase ( + id serial PRIMARY KEY, + name text NOT NULL, + telephone text NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE table menu_purchase ( + id serial PRIMARY KEY, + quantity numeric default 0, + menu_id smallint, + purchase_id smallint, + FOREIGN KEY (menu_id) REFERENCES menu (id), + FOREIGN KEY (purchase_id) REFERENCES purchase (id) +); + +-- insert items into menu table + +INSERT INTO menu (item, price, img, course) VALUES ('Mixed Salad', 6, 'mixed-salad.jpg', 'starter'); +INSERT INTO menu (item, price, img, course) VALUES ('Fried Chicken', 7, 'fried-chicken.jpg', 'starter'); +INSERT INTO menu (item, price, img, course) VALUES ('Vegetable soup', 5, 'vegetable-soup.jpg', 'starter'); +INSERT INTO menu (item, price, img, course) VALUES ('Beef stew', 9, 'beef-stew.jpg', 'main'); +INSERT INTO menu (item, price, img, course) VALUES ('Fish & chips', 11, 'fish-chips.jpg', 'main'); +INSERT INTO menu (item, price, img, course) VALUES ('Pork chops & vegetables', 9, 'pork-and-veg.jpg', 'main'); +INSERT INTO menu (item, price, img, course) VALUES ('Chocolate cake', 4, 'chocolate-cake.jpg', 'dessert'); +INSERT INTO menu (item, price, img, course) VALUES ('Blueberry cheesecake', 3.50, 'blueberry-cheesecake.jpg', 'dessert'); +INSERT INTO menu (item, price, img, course) VALUES ('Trifle', 3, 'trifle.jpg', 'dessert'); + +-- update for any changes +-- UPDATE menu SET item = 'Pork chops & vegetables', price = 10, img = 'pork-and-veg.jpg' WHERE id = 6 + +-- alter columns for any changes +-- ALTER TABLE purchase DROP COLUMN time; +-- ALTER TABLE purchase ADD COLUMN time TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/multiple-promise-jim.js b/multiple-promise-jim.js new file mode 100644 index 0000000..d8e22e4 --- /dev/null +++ b/multiple-promise-jim.js @@ -0,0 +1,32 @@ +const purchases = { + "items": { + "1": { + "id": 1, + "quantity": 2 + }, + "2": { + "id": 2, + "quantity": 3 + } + }, + "name": "Matt", + "tel": "07901 972 811" +} + +app.post("/order", (req, res) => { + // 1. insert into "order" table + db.one("INSERT INTO order (id) VALUES (DEFAULT) RETURNING id") + .then(result => { + const orderId = result.id; + const { items } = req.body; + // 2. insert into "order_item" table for each item + return Promise.all(items.map(item => { + return db.none( + "INSERT INTO order_item (menu_id, order_id, quantity) VALUES ($1, $2, $3)", + [item.menuItemId, orderId, item.quantity] + ); + })).then(() => orderId); + }) + .then(orderId => res.json({ orderId: orderId })) + .catch(error => res.json({ error: error.message })); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 167f7e9..94e168c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "react-testing", + "name": "lovely-grubbly", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -2011,6 +2011,37 @@ "integrity": "sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA==", "dev": true }, + "bl": { + "version": "0.9.5", + "resolved": "http://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", + "requires": { + "readable-stream": "~1.0.26" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -2055,6 +2086,14 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.x.x" + } + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -2382,9 +2421,9 @@ } }, "caniuse-db": { - "version": "1.0.30000890", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000890.tgz", - "integrity": "sha512-aO5uw1Taw8GkNMMXIWOz/WJz3y6tR1ETUAdH/pvO5EoJ3I1Po9vNJd9aMjY1GKucS/OXWMiQbXRbk3O1sgCbRA==", + "version": "1.0.30000921", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000921.tgz", + "integrity": "sha512-sAvmRuxZ457rlTK+ydUMpmeXjVfkiXQXv0POTdpHEdKrVwEQaeZqJgQA5MH7sKAGTGxzlLcDpfoNkpVXw09X5Q==", "dev": true }, "caniuse-lite": { @@ -2908,7 +2947,7 @@ }, "color-string": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "resolved": "http://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", "dev": true, "requires": { @@ -2943,8 +2982,7 @@ "commander": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" }, "commondir": { "version": "1.0.1", @@ -3124,6 +3162,14 @@ "which": "^1.2.9" } }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.x.x" + } + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -3190,9 +3236,9 @@ } }, "css-selector-tokenizer": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", - "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", "dev": true, "requires": { "cssesc": "^0.1.0", @@ -3202,7 +3248,7 @@ "dependencies": { "regexpu-core": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", "dev": true, "requires": { @@ -3288,6 +3334,11 @@ "cssom": "0.3.x" } }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -3626,8 +3677,7 @@ "duplexer": { "version": "0.1.1", "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" }, "duplexer3": { "version": "0.1.4", @@ -3936,19 +3986,27 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "event-stream": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz", - "integrity": "sha512-dGXNg4F/FgVzlApjzItL+7naHutA3fDqbV/zAZqDDlXTjiMnQmZKu+prImWKszeBM5UQeGvAl3u1wBiKeDh61g==", - "dev": true, + "version": "3.3.4", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "requires": { - "duplexer": "^0.1.1", - "flatmap-stream": "^0.1.0", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + }, + "dependencies": { + "split": { + "version": "0.3.3", + "resolved": "http://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "requires": { + "through": "2" + } + } } }, "events": { @@ -4039,13 +4097,13 @@ } }, "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", "requires": { "accepts": "~1.3.5", "array-flatten": "1.1.1", - "body-parser": "1.18.2", + "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", @@ -4062,10 +4120,10 @@ "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", + "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", @@ -4075,66 +4133,10 @@ "vary": "~1.1.2" }, "dependencies": { - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "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 < 2" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "statuses": { "version": "1.4.0", @@ -4533,9 +4535,9 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fastparse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", - "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", "dev": true }, "fb-watchman": { @@ -4651,12 +4653,6 @@ "readable-stream": "^2.0.2" } }, - "flatmap-stream": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/flatmap-stream/-/flatmap-stream-0.1.1.tgz", - "integrity": "sha512-lAq4tLbm3sidmdCN8G3ExaxH7cUCtP5mgDvrYowsx84dcYkJJ4I28N7gkxA6+YlSXzaGLJYIDEi9WGfXzMiXdw==", - "dev": true - }, "flatten": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", @@ -4738,8 +4734,7 @@ "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" }, "from2": { "version": "2.3.0", @@ -5314,6 +5309,22 @@ "globule": "^1.0.0" } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "requires": { + "is-property": "^1.0.0" + } + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -5777,6 +5788,17 @@ "minimalistic-assert": "^1.0.0" } }, + "hawk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-2.3.1.tgz", + "integrity": "sha1-HnMc45RH+h0PbXB/e87r7A/R7B8=", + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, "hbs": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.0.1.tgz", @@ -5823,6 +5845,11 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -6100,9 +6127,9 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, "ipaddr.js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", - "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" }, "is-absolute-url": { "version": "2.1.0", @@ -6253,6 +6280,23 @@ "is-path-inside": "^1.0.0" } }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" + }, + "is-my-json-valid": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", + "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", @@ -6348,6 +6392,11 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -7081,6 +7130,11 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -7092,6 +7146,11 @@ "verror": "1.10.0" } }, + "jwt-simple": { + "version": "0.1.0", + "resolved": "http://registry.npmjs.org/jwt-simple/-/jwt-simple-0.1.0.tgz", + "integrity": "sha1-VGs0qrAuPNScQ6QnlJizTZQAQeM=" + }, "keyv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", @@ -7679,10 +7738,9 @@ "dev": true }, "map-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", - "dev": true + "version": "0.1.0", + "resolved": "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" }, "map-visit": { "version": "1.0.0", @@ -7827,9 +7885,9 @@ } }, "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" }, "merge-descriptors": { "version": "1.0.1", @@ -8243,9 +8301,9 @@ } }, "node-sass": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz", - "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -8263,18 +8321,36 @@ "nan": "^2.10.0", "node-gyp": "^3.8.0", "npmlog": "^4.0.0", - "request": "2.87.0", + "request": "^2.88.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" }, "dependencies": { + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, "chalk": { "version": "1.1.3", "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -8298,6 +8374,89 @@ "which": "^1.2.9" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, + "requires": { + "mime-db": "~1.37.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "strip-ansi": { "version": "3.0.1", "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -8312,6 +8471,12 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true } } }, @@ -9023,7 +9188,6 @@ "version": "0.0.11", "resolved": "http://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, "requires": { "through": "~2.3" } @@ -9397,9 +9561,9 @@ } }, "postcss-modules-extract-imports": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz", - "integrity": "sha1-ZhQOzs447wa/DT41XWm/WdFB6oU=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz", + "integrity": "sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==", "dev": true, "requires": { "postcss": "^6.0.1" @@ -9646,9 +9810,9 @@ } }, "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, "postcss-zindex": { @@ -9775,12 +9939,12 @@ } }, "proxy-addr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", - "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.6.0" + "ipaddr.js": "1.8.0" } }, "prr": { @@ -10986,6 +11150,11 @@ } } }, + "scmp": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-0.0.3.tgz", + "integrity": "sha1-NkjfLXKUZB5/eGc//CloHZutkHM=" + }, "scoped-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-1.0.0.tgz", @@ -11290,6 +11459,14 @@ "kind-of": "^3.2.0" } }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.x.x" + } + }, "sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -11476,13 +11653,11 @@ } }, "stream-combiner": { - "version": "0.2.2", - "resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", - "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "dev": true, + "version": "0.0.4", + "resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", "requires": { - "duplexer": "~0.1.1", - "through": "~2.3.4" + "duplexer": "~0.1.1" } }, "stream-each": { @@ -11563,6 +11738,11 @@ "safe-buffer": "~5.1.0" } }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -11659,7 +11839,7 @@ "dependencies": { "colors": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "resolved": "http://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, @@ -12198,6 +12378,187 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "twilio": { + "version": "2.1.1", + "resolved": "http://registry.npmjs.org/twilio/-/twilio-2.1.1.tgz", + "integrity": "sha1-ysTw0+2D+Gr7GTflJckXxM7ojsY=", + "requires": { + "jwt-simple": "0.1.x", + "q": "0.9.7", + "request": "2.55.x", + "scmp": "0.0.3", + "underscore": "1.x" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=" + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=" + }, + "async": { + "version": "0.9.2", + "resolved": "http://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, + "aws-sign2": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=" + }, + "bluebird": { + "version": "2.11.0", + "resolved": "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "caseless": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.9.0.tgz", + "integrity": "sha1-t7Zc5r8UE4hlOc/VM/CzDv+pz4g=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "combined-stream": { + "version": "0.0.7", + "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "requires": { + "delayed-stream": "0.0.5" + } + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" + }, + "form-data": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz", + "integrity": "sha1-Jvi8JtpkQOKZy9z7aQNcT3em5GY=", + "requires": { + "async": "~0.9.0", + "combined-stream": "~0.0.4", + "mime-types": "~2.0.3" + } + }, + "har-validator": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-1.8.0.tgz", + "integrity": "sha1-2DhCsOtMQ1lgrrEIoGejqpTA7rI=", + "requires": { + "bluebird": "^2.9.30", + "chalk": "^1.0.0", + "commander": "^2.8.1", + "is-my-json-valid": "^2.12.0" + } + }, + "http-signature": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", + "requires": { + "asn1": "0.1.11", + "assert-plus": "^0.1.5", + "ctype": "0.5.3" + } + }, + "mime-db": { + "version": "1.12.0", + "resolved": "http://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", + "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" + }, + "mime-types": { + "version": "2.0.14", + "resolved": "http://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", + "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", + "requires": { + "mime-db": "~1.12.0" + } + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "oauth-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.6.0.tgz", + "integrity": "sha1-fb6uRPbKRU4fFoRR1jB0ZzWBPOM=" + }, + "q": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz", + "integrity": "sha1-TeLmyzspCIyeTLwDv51C+5bOL3U=" + }, + "qs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz", + "integrity": "sha1-9854jld33wtQENp/fE5zujJHD1o=" + }, + "request": { + "version": "2.55.0", + "resolved": "http://registry.npmjs.org/request/-/request-2.55.0.tgz", + "integrity": "sha1-11wc32eddrsQD5v/4f5VG1wk6T0=", + "requires": { + "aws-sign2": "~0.5.0", + "bl": "~0.9.0", + "caseless": "~0.9.0", + "combined-stream": "~0.0.5", + "forever-agent": "~0.6.0", + "form-data": "~0.2.0", + "har-validator": "^1.4.0", + "hawk": "~2.3.0", + "http-signature": "~0.10.0", + "isstream": "~0.1.1", + "json-stringify-safe": "~5.0.0", + "mime-types": "~2.0.1", + "node-uuid": "~1.4.0", + "oauth-sign": "~0.6.0", + "qs": "~2.4.0", + "stringstream": "~0.0.4", + "tough-cookie": ">=0.12.0", + "tunnel-agent": "~0.4.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + } + } + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -12303,8 +12664,7 @@ "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" }, "union-value": { "version": "1.0.0", diff --git a/package.json b/package.json index a03a776..cfa09eb 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,17 @@ { - "name": "deliveat-with-db", + "engines": { + "node": "10.9.0" + }, + "name": "lovely-grubbly", "version": "1.0.0", - "description": "An app which allows customers to order food online", + "description": "Order takeaway food online for delivery", "main": "server.js", "scripts": { "test": "jest", "dev": "webpack --mode development", - "build": "webpack --mode production" + "build": "webpack --mode production", + "start": "nodemon server.js", + "heroku-postbuild": "npm run build" }, "author": "", "license": "ISC", @@ -19,12 +24,14 @@ "dependencies": { "body-parser": "^1.18.3", "dotenv": "^6.1.0", - "express": "^4.16.3", + "event-stream": "3.3.4", + "express": "^4.16.4", "hbs": "^4.0.1", "jest": "^22.3.0", "pg-promise": "^8.5.0", "react": "^16.2.0", - "react-dom": "^16.2.0" + "react-dom": "^16.2.0", + "twilio": "^2.1.1" }, "devDependencies": { "babel-jest": "^22.4.1", @@ -37,12 +44,23 @@ "identity-obj-proxy": "^3.0.0", "jest": "^22.4.2", "jest-fetch-mock": "^1.6.6", - "node-sass": "^4.9.1", + "node-sass": "^4.11.0", "nodemon": "1.17.5", - "sass-loader": "^7.0.3", - "style-loader": "^0.21.0", "react-test-renderer": "^16.2.0", + "sass-loader": "^7.1.0", + "style-loader": "^0.21.0", "webpack": "^4.0.1", "webpack-cli": "^2.0.9" - } + }, + "directories": { + "test": "tests" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rolandjlevy/lovely-grubbly.git" + }, + "bugs": { + "url": "https://github.com/rolandjlevy/lovely-grubbly/issues" + }, + "homepage": "https://github.com/rolandjlevy/lovely-grubbly#readme" } diff --git a/server.js b/server.js index 89ca1de..3ede5ee 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,157 @@ +// server.js const express = require('express'); const bodyParser = require('body-parser'); const app = express(); +require('dotenv').config(); app.use(bodyParser.json()); -app.use('/static', express.static('static')); +app.use('/static', express.static('static')); app.set('view engine', 'hbs'); +const pgp = require('pg-promise')(); +const db = pgp({ + host: process.env.DB_HOST, + port: 5432, + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +// dummy menu object to show object structure const menu = { 1: { + course: "starter", id: 1, - name: "Strawberry cheesecake", - price: 6 + img: "mixed-salad.jpg", + item: "Mixed Salad", + price: "6.00" + }, + 2: { + course: "starter", + id: 2, + img: "fried-chicken.jpg", + item: "Fried Chicken", + price: "7.00" } -}; +} + +// dummy menu_purchases object to show object structure +const menu_purchases = { + items: { + 1: { + id: 1, + quantity: 2 + }, + 2: { + id: 2, + quantity: 4 + } + }, + name: "Matt", + telephone: "07901 972 811" +} app.get('/', function(req, res){ res.render('index'); }); -app.listen(8080, function(){ - console.log('Listening on port 8080'); +// get all menu items from menu table +app.get('/api/menu', function(req, res){ + db.any('SELECT * FROM menu') + .then(menu => { + const menuObject = menu.reduce((acc, item) => { + acc[item.id] = item; + return acc; + }, {}); + return res.json(menuObject); + }) + .catch(function(error) { + res.json({error: error.message}); + }); +}); + +// get all menu purchases from menu_purchase table +app.get('/api/menu_purchases', function(req, res){ + db.any('SELECT * FROM menu_purchase') + .then(menuPurchases => { + const purchasesObject = menuPurchases.reduce((acc, item) => { + acc[item.id] = item + return acc; + }, {}) + return res.json(purchasesObject); + }) + .catch(function(error) { + res.json({error: error.message}); + }); +}); + +// get all purchases from purchase table +app.get('/api/purchases', function(req, res){ + db.any('SELECT * FROM purchase') + .then(purchases => { + const purchasesObject = purchases.reduce((acc, item) => { + acc[item.id] = item + return acc; + }, {}) + return res.json(purchasesObject); + }) + .catch(function(error) { + res.json({error: error.message}); + }); }); + +// add a single purchase to menu_purchase table +app.post('/api/purchase', (req, res) =>{ + // 1. insert items, name, telephone and time from body request into "purchase" table + const { items, name, telephone } = req.body; + db.one(`INSERT INTO purchase (name, telephone, created_at) VALUES($1, $2, NOW()) RETURNING id`, [name, telephone]) + .then(result => { + // set RETURNING id from INSERT INTO + const orderId = result.id; + // 2. insert into "menu_purchase" table for each item + return Promise.all(Object.values(items).map(item => { + return db.none(`INSERT INTO menu_purchase (quantity, menu_id, purchase_id) VALUES($1, $2, $3)`, [item.quantity, item.id, orderId]); + })) + .then(() => orderId); // orderId is return when Promise.all is complete + }) + .then(orderId => { + sendSMS(orderId, name, telephone); + return res.json({ orderId, name, telephone }) + }) + .catch(error => res.json({ error: error.message })); +}); + +// get a single purchase from menu_purchase table +app.get('/api/purchase/:id', function(req, res){ + db.any('SELECT * FROM menu_purchase WHERE purchase_id=$1', [req.params.id]) + .then(menuPurchase => { + if (menuPurchase.length) { + return res.json(menuPurchase) + } else { + res.json({error: `Error: Your order with ID = ${req.params.id} could not be found, please contact us for help`}); + } + }) + .catch(function(error) { + res.json({error: error.message}); + }); +}); + +// https://www.twilio.com/docs/libraries/node +function sendSMS(orderId, customerName, customerTel) { + const accountSid = process.env.TWILIO_SID_LIVE // Your Account SID from www.twilio.com/console + const authToken = process.env.TWILIO_AUTH_LIVE; // Your Auth Token from www.twilio.com/console + const twilio = require('twilio'); + const client = new twilio(accountSid, authToken); + const baseUrl = 'https://lovely-grubbly.herokuapp.com'; + client.messages.create({ + body: `Dear ${customerName}, thank you for your order. Your ID is ${orderId}. To view your order details, please visit ${baseUrl}/?viewPurchaseId=${orderId}`, + to: customerTel, // Text this number + from: '+447446494074' // From a valid Twilio number + }) + .then((message) => console.log(message.sid)); +} + +const port = process.env.PORT || 8080; +app.listen( port, function(){ + console.log(`Listening on port number ${port}`); +}); \ No newline at end of file diff --git a/src/components/App.js b/src/components/App.js index 2ba4c77..a6c13c6 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,19 +1,232 @@ import React from 'react'; - +import Header from './Header'; +import MenuItems from './MenuItems'; +import ViewPurchase from './ViewPurchase'; +import ViewPurchaseById from './ViewPurchaseById'; import '../styles/App.scss'; +import '../styles/Tooltip.scss'; class App extends React.Component { constructor(){ super(); + + this.getCurrency = this.getCurrency.bind(this); + this.getMenuArray = this.getMenuArray.bind(this); + this.getMenuItembyId = this.getMenuItembyId.bind(this); + this.receiveCurrentPurchase = this.receiveCurrentPurchase.bind(this); + this.handlePurchaseId = this.handlePurchaseId.bind(this); + this.getPurchaseById = this.getPurchaseById.bind(this); + this.getAllMenuPurchases = this.getAllMenuPurchases.bind(this); + this.getAllPurchases = this.getAllPurchases.bind(this); + this.addSinglePurchase = this.addSinglePurchase.bind(this); + this.resetPurchaseId = this.resetPurchaseId.bind(this); + this.receiveFormInput = this.receiveFormInput.bind(this); + this.togglePurchaseBasket = this.togglePurchaseBasket.bind(this); + + + this.state = { + menu: null, + menuPurchases: {}, + purchaseIdForGet: null, + purchaseIdFromSuccess: null, + displayPurchaseById: null, + currentPurchase: null, + purchaseBasketVisible: false, + viewPurchaseId: window.location.search.split('viewPurchaseId=')[1] + } + } + + // Main features + /////////////////////////////////////////// + // 1. Customer places an order + // 2. Receive confirmation by text with a link to view the order online + // 3. Customer can retreive an order by entering their order ID + // 4. Admin can view all orders + + // initialise + /////////////////////////////////////////// + + componentDidMount () { + this.getAllMenuItems(); + this.getAllMenuPurchases(); + this.getAllPurchases(); + } + + // utilities + /////////////////////////////////////////// + + getCurrency (string) { + return string.toLocaleString("en-GB", { + style: "currency", + currency: "GBP" + }); + } + + displayAsJSON (object) { + return
{JSON.stringify(object, null, 2)}
} + + // get menu data + /////////////////////////////////////////// + + getAllMenuItems () { + fetch("/api/menu") + .then(response => response.json()) + .then(menu => { + this.setState({ menu }) + }) + .catch(error => res.json({ error: error.message })); + } + + getMenuArray () { + return Object.values(this.state.menu); + } + + getMenuItembyId (id) { + return this.state.menu[id]; + } + + + // get purchase by id + /////////////////////////////////////////// + + handlePurchaseId (e) { // update purchaseId + if (typeof e === 'string') { + this.setState({ purchaseId: e }); + } else { + this.setState({ purchaseId: e.target.value }); + } + } + + getPurchaseById (event) { // fetch from /api/purchase/$id + event.preventDefault(); + const id = this.state.purchaseId; + if (!id.length || id === "0" || isNaN(id)) return; + fetch(`/api/purchase/${id}`) + .then(response => response.json()) + .then(purchase => { + this.setState({ displayPurchaseById: purchase }) + }) + .catch(error => res.json({ error: error.message })); + } + + receiveCurrentPurchase (currentPurchase) { // receive currentPurchase from MenuItems + this.setState({ currentPurchase }) + } + + // add a single purchase + /////////////////////////////////////////// + + addSinglePurchase (event) { + event.preventDefault(); + if (!this.state.currentPurchase.name || !this.state.currentPurchase.telephone) return; + fetch('/api/purchase', { + method: 'post', + body: JSON.stringify(this.state.currentPurchase), + headers: { 'Content-Type': 'application/json' } + } + ).then(response => response.json() + ).then(orderId => { + this.setState({ purchaseIdFromSuccess: orderId }) + this.getAllMenuPurchases(); + this.getAllPurchases(); + }) + .catch(error => res.json({ error: error.message })); + } + + resetPurchaseId () { + this.setState({ + purchaseIdFromSuccess: null, + currentPurchase: null + }) + } + + receiveFormInput (event) { + const currentPurchase = Object.assign({}, this.state.currentPurchase, {[event.target.name]: event.target.value}) + this.setState({ currentPurchase }) + } + + // get all menu_purchases + /////////////////////////////////////////// + + getAllMenuPurchases () { + fetch(`/api/menu_purchases`) + .then(response => response.json()) + .then(menuPurchases => { + this.setState({ menuPurchases }) + }) + .catch(error => res.json({ error: error.message })); + } + + // get all purchases + /////////////////////////////////////////// + + getAllPurchases () { + fetch(`/api/purchases`) + .then(response => response.json()) + .then(purchases => { + this.setState({ purchases }) + }) + .catch(error => res.json({ error: error.message })); + } + + togglePurchaseBasket (event) { + event.preventDefault(); + if (this.state.currentPurchase) { + this.setState({ purchaseBasketVisible: !this.state.purchaseBasketVisible }); + } + } + + render() { + + const header = +
+ + // customer views their purchase + const viewPurchaseById = this.state.viewPurchaseId && + + + // view basket (currentPurchase) + const viewPurchase = this.state.currentPurchase && this.state.purchaseBasketVisible && + + + const menuItems = this.state.menu && + - render(){ return ( -
- Delivereat app -
+ + { header } + { viewPurchaseById } + { viewPurchase } + { menuItems } + ) } } -export default App; +export default App; \ No newline at end of file diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..124c673 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,26 @@ +import React from 'react'; +import '../styles/Header.scss'; + +function Header ( { currentPurchase, togglePurchaseBasket } ) { + const basketCount = currentPurchase && (Object.values(currentPurchase.items).filter(item => typeof item === "object").length || null); + return ( +
+

Lovely Grubbly

+
+

{basketCount}

+

+ + + +

+

+ + + +

+
+
+ ) +} + +export default Header; \ No newline at end of file diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js new file mode 100644 index 0000000..cd9c109 --- /dev/null +++ b/src/components/MenuItem.js @@ -0,0 +1,106 @@ +import React from 'react'; +import '../styles/MenuItem.scss'; +import '../styles/Checkbox.scss'; + +class MenuItem extends React.Component { + constructor () { + super(); + this.incQuantity = this.incQuantity.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.validateQuantity = this.validateQuantity.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleFocus = this.handleFocus.bind(this); + this.handleBlur = this.handleBlur.bind(this); + + this.state = { + quantity: 0, + inputEditable: false + } + } + + incQuantity (amount, event) { + event.preventDefault(); + const keepPositive = n => (n < 0 ? 0 : Number(n)); + const inputValue = Number(event.currentTarget.parentNode.parentNode.parentNode.parentNode.quantity.value); + event.currentTarget.parentNode.parentNode.parentNode.parentNode.quantity.value = keepPositive(inputValue + Number(amount)); + this.setState({ + quantity: keepPositive(inputValue + Number(amount)) + }) + } + + handleSubmit (event) { + event.preventDefault(); + const id = this.props.item.id; + let currentPurchase; + const initialCurrentPurchase = { [id]: {id: id, quantity: this.state.quantity} } + if (this.state.quantity > 0) { + if (this.props.currentPurchase) { + currentPurchase = Object.assign({}, this.props.currentPurchase, { items: Object.assign({}, this.props.currentPurchase.items, initialCurrentPurchase )}); + } else { + currentPurchase = Object.assign({}, this.props.currentPurchase, { items: Object.assign({}, initialCurrentPurchase )}); + } + } else { + if (this.props.currentPurchase) { + currentPurchase = this.props.currentPurchase; + delete currentPurchase.items[id]; + } else { + return; + } + } + this.props.receiveCurrentPurchase(currentPurchase); + } + + validateQuantity (n) { + if (this.state.quantity > 0 && !this.props.currentPurchase) { + this.setState({ quantity: 0 }); + return n; + } else { + return this.state.quantity; + } + } + + shouldComponentUpdate() { + return true; + } + + handleFocus (event) { + event.preventDefault(); + } + + handleBlur (event) { + event.preventDefault(); + } + + handleInputChange (event) { + event.preventDefault(); + } + + render () { + const imagePath = `../static/images/${this.props.item.img}`; + return ( +
+ +
    +
  • {this.props.item.item}
  • +
  • Price: £{this.props.getCurrency(this.props.item.price)}
  • +
    +
  • +
      +
    • +
    • +
    • +
    +
  • +
    +
  • + +
  • +
+
+ ) + } +} + +export default MenuItem; \ No newline at end of file diff --git a/src/components/MenuItems.js b/src/components/MenuItems.js new file mode 100644 index 0000000..3b46a40 --- /dev/null +++ b/src/components/MenuItems.js @@ -0,0 +1,97 @@ +import React from 'react'; +import MenuItem from './MenuItem'; + +class MenuItems extends React.Component { + constructor() { + super() + this.handleMenuSelection = this.handleMenuSelection.bind(this); + this.menuSelectionCheckboxes = this.menuSelectionCheckboxes.bind(this); + this.state = { + courses: { starter: 'Starters', main: 'Mains', dessert: 'Desserts' }, + coursesSelected: {}, + loaded: null + } + } + + componentDidMount (){ + this.setState({ + loaded: true, + coursesSelected: Object.assign({}, this.state.courses) + }) + } + + handleMenuSelection(course, event) { + let courses; + if (event.target.checked) { + courses = Object.assign({}, this.state.coursesSelected, {[course]: this.state.courses[course] }) + } else { + courses = Object.assign({}, this.state.coursesSelected) + delete courses[course] + } + this.setState({ coursesSelected: courses }) + + } + + menuSelectionCheckboxes() { + return Object.keys(this.state.courses).map(course => { + const input = +
+ +
+ return input; + }) + } + + createMenuSection () { + return Object.keys(this.state.coursesSelected).sort().reverse().map(course => { + const menuSection = this.props.menuArray.filter(itemObject => { + return itemObject.course === course; + }) + .map(itemObj => { + const id = `${itemObj.course}-${itemObj.id}`; + return + }) + return ( +
+ {

{this.state.courses[course]}

} + {menuSection} +
+ )} + ) + } + + render () { + + const menuItems = this.state.loaded && + +
+ +
+
+ {this.menuSelectionCheckboxes()} +
+
+
+ {this.createMenuSection()} +
+
+
+ + return ( + + {menuItems} + + ) + } +} + +export default MenuItems; \ No newline at end of file diff --git a/src/components/ViewPurchase.js b/src/components/ViewPurchase.js new file mode 100644 index 0000000..0af4e62 --- /dev/null +++ b/src/components/ViewPurchase.js @@ -0,0 +1,77 @@ +import React from 'react'; +import '../styles/ViewPurchase.scss'; + +function ViewPurchase ({ resetPurchaseId, purchaseIdFromSuccess, receiveFormInput, addSinglePurchase, currentPurchase, getCurrency, menu }) { + + function getButtonClass() { + const className = currentPurchase.name && currentPurchase.telephone ? "" : "inactive"; + return className; + } + + const purchaseItems = Object.values(currentPurchase.items) || null; + let total = 0; + const deliveryCharge = 3; + const displayPurchaseItems = purchaseItems.length ? (
+
+
    + { purchaseItems.map(obj => { + const price = Number(menu[obj.id].price); + total += obj.quantity * price; + return ( +
  • + {obj.quantity} x {menu[obj.id].item} @ {getCurrency(price)} = {getCurrency(Number(obj.quantity * price))} +
    + +
    + +
    +
    +
  • + ); + })} +

  • +
  • Subtotal: {getCurrency(total)}
  • +
  • Delivery: {getCurrency(deliveryCharge)}
  • +
  • Total: {getCurrency(total + deliveryCharge)}
  • +

  • +
+
+
+
    +
  • +
  • +
  • +
+
+
) : 'Your basket is empty'; + + const state = purchaseIdFromSuccess === null ? ( + +
+

Your Basket

+
+ {displayPurchaseItems} +
) : + ( +
+

Your order

+
+
+ +
+
) + + return ( +
+ {state} +
+ ) +} + +export default ViewPurchase; \ No newline at end of file diff --git a/src/components/ViewPurchaseById.js b/src/components/ViewPurchaseById.js new file mode 100644 index 0000000..c29486d --- /dev/null +++ b/src/components/ViewPurchaseById.js @@ -0,0 +1,82 @@ +import React from 'react'; +import '../styles/ViewPurchase.scss'; + +class ViewPurchaseById extends React.Component { + constructor(){ + super(); + this.toggleView = this.toggleView.bind(this) + this.state = { + toggleState: true + } + } + + componentDidMount () { + if (this.props.viewPurchaseId) { + this.props.handlePurchaseId(this.props.viewPurchaseId); + } + } + + toggleView () { + this.setState({ toggleState: !this.state.toggleState }) + } + + getPurchaseId () { + return this.props.viewPurchaseId && (this.props.viewPurchaseId !== "0") ? this.props.viewPurchaseId : ""; + } + + render () { + + const inputPurchaseId = +
+
    +
  • View your order

  • +
  • Enter your order ID
  • +
  • +
  • +
+
+ + let total = 0; + let nameObj = false + const displayPurchaseId = this.props.displayPurchaseById && + Object.values(this.props.displayPurchaseById).map(item => { + const menuItem = this.props.menu[item.menu_id]; + const purchaseID = `purchaseId-${item.menu_id}`; + const subTotal = (Number(menuItem.price) * Number(item.quantity)); + total = total + subTotal; + nameObj = nameObj === false ?
  • Name: {this.props.purchases[item.purchase_id].name}
  • : null; + return ( item.id ? +
      + {nameObj} +
    • {item.quantity} x {menuItem.item} @ {this.props.getCurrency(menuItem.price)} = {this.props.getCurrency(subTotal)} +
      + +
      + +
      +
      +
    • +
    : item + ) + }); + + const delivery = 3; + const summary = this.props.displayPurchaseById && +
      +

    • +
    • Subtotal: {this.props.getCurrency(total)}
    • +
    • Delivery: {this.props.getCurrency(delivery)}
    • +
    • Total: {this.props.getCurrency(total + delivery)}
    • +
    + + return ( + this.state.toggleState &&
    + {inputPurchaseId} + {displayPurchaseId} + {summary} +
    + ) + } +} + +export default ViewPurchaseById; \ No newline at end of file diff --git a/src/styles/App.scss b/src/styles/App.scss index 0bdf5c0..76b8e67 100644 --- a/src/styles/App.scss +++ b/src/styles/App.scss @@ -1,3 +1,103 @@ +$border-thickness: 0.05rem; +$light-colour: #fffae8; +$mid-colour: #ff9e0d; +$mid-dark-colour: #ff7700; + +* { + box-sizing: border-box; +} + +html { + box-sizing: border-box; + scroll-behavior: smooth; + width: 100%; + height: 100%; + overflow: auto; + padding: 0; + margin: 0; + line-height: 2rem; +} + body { - background-color: Cornsilk; + padding: 0; + margin: 0; + box-sizing: border-box; + background-color: $light-colour; + font-family: 'Open Sans', Arial, Helvetica, sans-serif; + letter-spacing: 0.05rem; + font-size: 1rem; +} + +#root { + padding: 0; + margin: 0; +} + +.main { + margin: 0 1rem 1rem 1rem; + // overflow-y: auto; +} + +label { + cursor: pointer; +} + +input, button { + font-size: 1rem; + letter-spacing: 0.05rem; +} + +input { + border: 0.05rem solid #ccc; + color: #333; + padding: 0.5rem 0.7rem; +} + +button { + font-weight: 700; + background-color: $mid-colour; + border: 0; + color: white; + padding: 0.5rem 0.7rem; + &:hover { + cursor: pointer; + background-color: $mid-dark-colour; + } +} + +ul { + list-style: none; + margin: 0; + padding: 0; +} + +.hr-black { + background-color: #ccc; + border: 0 none; + color: #eee; + height: 1px; +} + +.hr-white { + border: 0.05rem solid white; + height: 0.05rem; + border: 0; +} + +.fas-padding-right { + padding-right: 0.5rem; } + +.fadein { + opacity: 1; + @-webkit-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } + @-moz-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } + @-ms-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } + @-o-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } + @keyframes fadein { from { opacity: 0; } to { opacity: 1; } } + -webkit-animation: fadein 0.5s; + -moz-animation: fadein 0.5s; + -ms-animation: fadein 0.5s; + -o-animation: fadein 0.5s; + animation: fadein 0.5s; +} \ No newline at end of file diff --git a/src/styles/Checkbox.scss b/src/styles/Checkbox.scss new file mode 100644 index 0000000..b722eea --- /dev/null +++ b/src/styles/Checkbox.scss @@ -0,0 +1,70 @@ +// https://www.w3schools.com/howto/howto_css_custom_checkbox.asp + +$mid-colour: #ff9e0d; + +/* Customize the label (the container) */ +.container { + display: block; + position: relative; + padding-left: 2rem; + cursor: pointer; + font-size: 1rem; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Hide the browser's default checkbox */ +.container input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +/* Create a custom checkbox */ +.checkmark { + position: absolute; + top: 0; + left: 0; + height: 1.5rem; + width: 1.5rem; + background-color: #eee; +} + +/* On mouse-over, add a grey background color */ +.container:hover input ~ .checkmark { + background-color: #ccc; +} + +/* When the checkbox is checked, add a blue background */ +.container input:checked ~ .checkmark { + background-color: $mid-colour; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.container input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.container .checkmark:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} \ No newline at end of file diff --git a/src/styles/Header.scss b/src/styles/Header.scss new file mode 100644 index 0000000..c84c626 --- /dev/null +++ b/src/styles/Header.scss @@ -0,0 +1,66 @@ + +$header-colour: #ff9e0d; +$mid-colour: #fff5dc; +$light-colour: white; + +.fas { + margin-left: 0.5rem; +} + +.header { + background-color: $header-colour; + padding: 0 1rem; + margin: 0; + display: block; + min-width: 50%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + & a { + color: white; + text-decoration: none; + opacity: 1; + &:hover { + cursor: pointer; + opacity: 0.8; + } + } + &__title { + padding: 0; + margin: 0.5rem 0 0 0; + letter-spacing: 0.3rem; + color: $light-colour; + font-family: 'Luckiest Guy', cursive; + font-size: 1.5rem; + filter: drop-shadow(0 0 3px unquote('#ab660050')); + } + &__items { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + filter: drop-shadow(0 0 3px unquote('#ab660050')); + margin: 0; + padding: 0; + font-size: 1rem; + &-count { + font-size: 1rem; + opacity: 1; + color: $light-colour; + margin: 0 0 1.5rem 0; + padding: 0; + } + &-toggle { + font-size: 1.5rem; + color: $light-colour; + opacity: 1; + margin: 0; + padding: 0; + &:hover { + cursor: pointer; + opacity: 0.8; + } + } + } + } diff --git a/src/styles/MenuItem.scss b/src/styles/MenuItem.scss new file mode 100644 index 0000000..e97efa4 --- /dev/null +++ b/src/styles/MenuItem.scss @@ -0,0 +1,90 @@ +$mid-colour: #ff9e0d; + +.menu { + font-family: 'Luckiest Guy', cursive; + color: $mid-colour; + font-size: 1.5rem; + letter-spacing: 0.2rem; + margin: 2.5rem 0 0.5rem 0; + &__selection { + padding: 1rem; + background-color: #fff; + display: flex; + flex-direction: row; + justify-content: flex-start; + } + &__sections { + // overflow-y: auto; + } + &__checkbox { + padding-right: 1rem; + &-choice { + background-color: #ab6600; + } + } + &__item { + padding: 0; + display: flex; + flex-direction: row; + justify-content: space-between; + background-color: #fff; + filter: drop-shadow(0 0 0.3rem unquote('#ab660030')); + margin: 0 0 0.5rem 0; + max-width: 24rem; + min-width: 18rem; + &-details { + margin: 1rem 1rem 1rem 0.5rem; + display: flex; + flex-direction: column; + list-style: none; + } + &-amount { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + & li { + min-width: 2rem; + text-align: center; + } + border: 0.05rem solid $mid-colour; + margin: 0 0 1rem 0; + input { + width: 4.5rem; + height: 1.8rem; + text-align: center; + border: 0; + } + } + img { + border: 0.25rem solid #fff; + object-fit: cover; + height: 9rem; + width: 12rem; + min-height: 6rem; + min-width: 8rem; + // max-width: 200px; + margin: 1rem 0.5rem 1rem 1rem; + } + img[class] { + height: 9rem; + width: 12rem; + } + button { + min-width: 2rem; + font-size: 1rem; + } + } +} + +.hero-container { + margin: 0; + padding: 0; + height: 100px; +} + +.hero { + width: 100%; + height: 100px; + object-fit: cover; +} \ No newline at end of file diff --git a/src/styles/Tooltip.scss b/src/styles/Tooltip.scss new file mode 100644 index 0000000..d8493bc --- /dev/null +++ b/src/styles/Tooltip.scss @@ -0,0 +1,43 @@ +$dark-colour: #2d2211; + +.tooltip { + position: relative; + display: inline-block; + cursor: pointer; + & .tooltip-content { + visibility: hidden; + top: -18px; + left: 40px; + position: absolute; + z-index: 1; + opacity: 0; + transition: opacity 0.5s; + cursor: auto; + & img { + padding: 0; + margin: 0; + width: 92px; + min-width: 92px; + border-radius: 4px; + } + &::after { + content: ""; + position: absolute; + top: 50%; + right: 100%; + margin-top: -10px; + border: 7px solid $dark-colour; + border-color: transparent $dark-colour transparent transparent; + } + } +} + +.tooltip:hover .tooltip-content { + visibility: visible; + opacity: 1; +} + +.fa-icon-style { + margin: 0.4rem 0 0 0.5rem; + font-size: 130%; +} \ No newline at end of file diff --git a/src/styles/ViewPurchase.scss b/src/styles/ViewPurchase.scss new file mode 100644 index 0000000..b5282ee --- /dev/null +++ b/src/styles/ViewPurchase.scss @@ -0,0 +1,64 @@ +$mid-colour: #ff9e0d; + +.purchase { + position: absolute; + z-index: 10; + background-color: #fff; + display: flex; + flex-direction: column; + margin: 0; + padding: 1rem; + min-width: 100%; + left: 0; + filter: drop-shadow(0 0 1rem unquote('#ab660050')); + &__header { + & h2 { + margin: 0.5rem 0; + } + } + & ul, & li { + margin: 0; + padding: 0; + } + &__checkout { + input, button { + min-width: 9rem; + width: 100%; + min-width: 12rem; + max-width: 24rem; + margin-bottom: 1em; + &.inactive { + background-color: #ccc; + } + } + & a { + color: $mid-colour; + &:hover { + text-decoration: none; + } + } + } + &__id { + input { + min-width: 9rem; + width: 100%; + min-width: 12rem; + max-width: 24rem; + margin-bottom: 1em; + } + } + &__id { + button { + width: 11rem; + min-width: 5rem; + max-width: 11rem; + margin-bottom: 1em; + &.cancel { + margin-right: 1rem; + } + &.details { + margin-left: 0; + } + } + } +} diff --git a/static/images/beef-stew.jpg b/static/images/beef-stew.jpg new file mode 100644 index 0000000..8104841 Binary files /dev/null and b/static/images/beef-stew.jpg differ diff --git a/static/images/blueberry-cheesecake.jpg b/static/images/blueberry-cheesecake.jpg new file mode 100644 index 0000000..aef1d1e Binary files /dev/null and b/static/images/blueberry-cheesecake.jpg differ diff --git a/static/images/chocolate-cake.jpg b/static/images/chocolate-cake.jpg new file mode 100644 index 0000000..35dd422 Binary files /dev/null and b/static/images/chocolate-cake.jpg differ diff --git a/static/images/fish-chips.jpg b/static/images/fish-chips.jpg new file mode 100644 index 0000000..ad8bd9d Binary files /dev/null and b/static/images/fish-chips.jpg differ diff --git a/static/images/fried-chicken.jpg b/static/images/fried-chicken.jpg new file mode 100644 index 0000000..ca1e6f0 Binary files /dev/null and b/static/images/fried-chicken.jpg differ diff --git a/static/images/homepage-hero.png b/static/images/homepage-hero.png new file mode 100644 index 0000000..9d81a87 Binary files /dev/null and b/static/images/homepage-hero.png differ diff --git a/static/images/mixed-salad.jpg b/static/images/mixed-salad.jpg new file mode 100644 index 0000000..7b57d77 Binary files /dev/null and b/static/images/mixed-salad.jpg differ diff --git a/static/images/picture-icon.png b/static/images/picture-icon.png new file mode 100644 index 0000000..6fdf47d Binary files /dev/null and b/static/images/picture-icon.png differ diff --git a/static/images/pork-and-veg.jpg b/static/images/pork-and-veg.jpg new file mode 100644 index 0000000..e738467 Binary files /dev/null and b/static/images/pork-and-veg.jpg differ diff --git a/static/images/trifle.jpg b/static/images/trifle.jpg new file mode 100644 index 0000000..2992b7f Binary files /dev/null and b/static/images/trifle.jpg differ diff --git a/static/images/vegetable-soup.jpg b/static/images/vegetable-soup.jpg new file mode 100644 index 0000000..16d8450 Binary files /dev/null and b/static/images/vegetable-soup.jpg differ diff --git a/views/body-html-structure.html b/views/body-html-structure.html new file mode 100644 index 0000000..55de459 --- /dev/null +++ b/views/body-html-structure.html @@ -0,0 +1,48 @@ + + +
    + +
    + +
    + +
    + + + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + \ No newline at end of file diff --git a/views/index.hbs b/views/index.hbs index 8fdb420..fcb7063 100644 --- a/views/index.hbs +++ b/views/index.hbs @@ -1,13 +1,20 @@ - Let's get some food + + + + Lovely Grubbly + + +
    - +
    +