summaryrefslogtreecommitdiffstats
path: root/finished-application/frontend/components
diff options
context:
space:
mode:
Diffstat (limited to 'finished-application/frontend/components')
-rw-r--r--finished-application/frontend/components/AddToCart.js68
-rw-r--r--finished-application/frontend/components/Cart.js74
-rw-r--r--finished-application/frontend/components/CartCount.js64
-rw-r--r--finished-application/frontend/components/CartItem.js53
-rw-r--r--finished-application/frontend/components/CreateItem.js148
-rw-r--r--finished-application/frontend/components/DeleteItem.js71
-rw-r--r--finished-application/frontend/components/Dump.js19
-rw-r--r--finished-application/frontend/components/EditUser.js81
-rw-r--r--finished-application/frontend/components/ErrorMessage.js51
-rw-r--r--finished-application/frontend/components/Header.js71
-rw-r--r--finished-application/frontend/components/Item.js53
-rw-r--r--finished-application/frontend/components/Items.js74
-rw-r--r--finished-application/frontend/components/LoadingItem.js22
-rw-r--r--finished-application/frontend/components/Meta.js16
-rw-r--r--finished-application/frontend/components/Nav.js59
-rw-r--r--finished-application/frontend/components/Order.js98
-rw-r--r--finished-application/frontend/components/OrderList.js93
-rw-r--r--finished-application/frontend/components/Page.js67
-rw-r--r--finished-application/frontend/components/Pagination.js97
-rw-r--r--finished-application/frontend/components/Permissions.js133
-rw-r--r--finished-application/frontend/components/PleaseSignIn.js46
-rw-r--r--finished-application/frontend/components/RemoveFromCart.js57
-rw-r--r--finished-application/frontend/components/Reset.js85
-rw-r--r--finished-application/frontend/components/ResetRequest.js55
-rw-r--r--finished-application/frontend/components/Search.js143
-rw-r--r--finished-application/frontend/components/Signin.js79
-rw-r--r--finished-application/frontend/components/Signout.js25
-rw-r--r--finished-application/frontend/components/Signup.js93
-rw-r--r--finished-application/frontend/components/SingleItem.js77
-rw-r--r--finished-application/frontend/components/TakeMyMoney.js83
-rw-r--r--finished-application/frontend/components/UpdateItem.js105
-rw-r--r--finished-application/frontend/components/User.js45
-rw-r--r--finished-application/frontend/components/styles/CartStyles.js47
-rw-r--r--finished-application/frontend/components/styles/CloseButton.js13
-rw-r--r--finished-application/frontend/components/styles/Form.js71
-rw-r--r--finished-application/frontend/components/styles/ItemStyles.js39
-rw-r--r--finished-application/frontend/components/styles/NavStyles.js64
-rw-r--r--finished-application/frontend/components/styles/OrderItemStyles.js44
-rw-r--r--finished-application/frontend/components/styles/OrderStyles.js38
-rw-r--r--finished-application/frontend/components/styles/PriceTag.js17
-rw-r--r--finished-application/frontend/components/styles/SickButton.js20
-rw-r--r--finished-application/frontend/components/styles/Supreme.js13
-rw-r--r--finished-application/frontend/components/styles/Table.js31
-rw-r--r--finished-application/frontend/components/styles/Title.js20
44 files changed, 2722 insertions, 0 deletions
diff --git a/finished-application/frontend/components/AddToCart.js b/finished-application/frontend/components/AddToCart.js
new file mode 100644
index 0000000..03242a4
--- /dev/null
+++ b/finished-application/frontend/components/AddToCart.js
@@ -0,0 +1,68 @@
+import { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import User, { CURRENT_USER_QUERY } from './User';
+
+const ADD_TO_CART_MUTATION = gql`
+ mutation addToCart($id: ID!) {
+ addToCart(id: $id) {
+ id
+ quantity
+ item {
+ id
+ price
+ description
+ image
+ title
+ }
+ }
+ }
+`;
+
+class AddToCart extends Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ };
+
+ update = (cache, payload) => {
+ const newCartItem = payload.data.addToCart;
+ const data = cache.readQuery({ query: CURRENT_USER_QUERY });
+
+ const existingIndex = data.me.cart.findIndex(cartItem => cartItem.id === newCartItem.id);
+ if (existingIndex >= 0) {
+ // already in cache, just replace it
+ data.me.cart = [
+ ...data.me.cart.slice(0, existingIndex),
+ newCartItem,
+ ...data.me.cart.slice(existingIndex + 1),
+ ];
+ } else {
+ data.me.cart = [...data.me.cart, newCartItem];
+ }
+ cache.writeQuery({ query: CURRENT_USER_QUERY, data });
+ };
+
+ render() {
+ const { id } = this.props;
+ return (
+ <User>
+ {({ data: { me }, loading }) => {
+ if (!me || loading) return null;
+ return (
+ <Mutation mutation={ADD_TO_CART_MUTATION} variables={{ id }} update={this.update}>
+ {(addToCart, { loading }) => (
+ <button disabled={loading} onClick={addToCart}>
+ 🛒 Add{loading && 'ing'} To Cart
+ </button>
+ )}
+ </Mutation>
+ );
+ }}
+ </User>
+ );
+ }
+}
+
+export default AddToCart;
+export { ADD_TO_CART_MUTATION };
diff --git a/finished-application/frontend/components/Cart.js b/finished-application/frontend/components/Cart.js
new file mode 100644
index 0000000..7e8a9c5
--- /dev/null
+++ b/finished-application/frontend/components/Cart.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Mutation, Query } from 'react-apollo';
+import { adopt } from 'react-adopt';
+import gql from 'graphql-tag';
+import TakeMyMoney from './TakeMyMoney';
+import formatMoney from '../lib/formatMoney';
+import CartItem from './CartItem';
+import { CURRENT_USER_QUERY } from './User';
+import calcTotalPrice from '../lib/calcTotalPrice';
+import Error from './ErrorMessage';
+import CartStyles from './styles/CartStyles';
+import Supreme from './styles/Supreme';
+import CloseButton from './styles/CloseButton';
+import SickButton from './styles/SickButton';
+
+const LOCAL_STATE_QUERY = gql`
+ query {
+ cartOpen @client
+ }
+`;
+
+const TOGGLE_CART_MUTATION = gql`
+ mutation {
+ toggleCart @client
+ }
+`;
+
+const Composed = adopt({
+ toggleCart: ({ render }) => (
+ <Mutation mutation={TOGGLE_CART_MUTATION}>
+ {(mutate, result) => render({ mutate, result })}
+ </Mutation>
+ ),
+ localState: ({ render }) => <Query query={LOCAL_STATE_QUERY} children={render} />,
+ currentUser: ({ render }) => (
+ <Query children={render} query={CURRENT_USER_QUERY} data-test="cart" />
+ ),
+});
+
+const Cart = () => (
+ <Composed>
+ {({ toggleCart, localState, currentUser }) => {
+ const { data: { me }, error, loading } = currentUser;
+ if (loading) return <p>Loading...</p>;
+ if (error) return <Error error={error} />;
+ if (!me) return null;
+ return (
+ <CartStyles open={localState.data.cartOpen}>
+ <header>
+ <CloseButton title="close" onClick={toggleCart.mutate}>
+ &times;
+ </CloseButton>
+
+ <Supreme>{me.name}'s Cart.</Supreme>
+ <p>
+ You have {me.cart.length} item{me.cart.length === 1 ? '' : 's'} in your cart.
+ </p>
+ </header>
+
+ <ul>{me.cart.map(cartItem => <CartItem key={cartItem.id} cartItem={cartItem} />)}</ul>
+ <footer>
+ <p>{formatMoney(calcTotalPrice(me.cart))}</p>
+ <TakeMyMoney>
+ <SickButton>Checkout</SickButton>
+ </TakeMyMoney>
+ </footer>
+ </CartStyles>
+ );
+ }}
+ </Composed>
+);
+
+export default Cart;
+export { LOCAL_STATE_QUERY, TOGGLE_CART_MUTATION };
diff --git a/finished-application/frontend/components/CartCount.js b/finished-application/frontend/components/CartCount.js
new file mode 100644
index 0000000..ae51f20
--- /dev/null
+++ b/finished-application/frontend/components/CartCount.js
@@ -0,0 +1,64 @@
+import styled from 'styled-components';
+import { TransitionGroup, CSSTransition } from 'react-transition-group';
+import PropTypes from 'prop-types';
+
+const Dot = styled.div`
+ background: ${props => props.theme.red};
+ color: white;
+ border-radius: 50%;
+ padding: 0.5rem;
+ line-height: 2rem;
+ min-width: 3rem;
+ margin-left: 1rem;
+ font-weight: 100;
+ font-feature-settings: 'tnum';
+ font-variant-numeric: tabular-nums;
+`;
+
+const AnimationStyles = styled.span`
+ position: relative;
+ .count {
+ display: block;
+ position: relative;
+ transition: all 0.4s;
+ backface-visibility: hidden;
+ }
+ .count-enter {
+ transform: rotateX(0.5turn);
+ }
+
+ .count-enter-active {
+ transform: rotateX(0);
+ }
+
+ .count-exit {
+ top: 0;
+ position: absolute;
+ transform: rotateX(0);
+ }
+
+ .count-exit-active {
+ transform: rotateX(0.5turn);
+ }
+`;
+
+const CartCount = ({ count }) => (
+ <AnimationStyles>
+ <TransitionGroup>
+ <CSSTransition
+ unmountOnExit
+ className="count"
+ classNames="count"
+ key={count}
+ timeout={{ enter: 400, exit: 400 }}
+ >
+ <Dot>{count}</Dot>
+ </CSSTransition>
+ </TransitionGroup>
+ </AnimationStyles>
+);
+
+CartCount.propTypes = {
+ count: PropTypes.number.isRequired,
+};
+export default CartCount;
diff --git a/finished-application/frontend/components/CartItem.js b/finished-application/frontend/components/CartItem.js
new file mode 100644
index 0000000..6c6d575
--- /dev/null
+++ b/finished-application/frontend/components/CartItem.js
@@ -0,0 +1,53 @@
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import formatMoney from '../lib/formatMoney';
+import RemoveFromCart from './RemoveFromCart';
+
+const CartItemStyles = styled.li`
+ padding: 1rem 0;
+ border-bottom: 1px solid ${props => props.theme.lightgrey};
+ display: grid;
+ align-items: center;
+ grid-template-columns: auto 1fr auto;
+ img {
+ margin-right: 10px;
+ }
+ h3 {
+ margin: 0;
+ }
+ p {
+ margin: 0;
+ }
+`;
+
+const CartItem = ({ cartItem }) => {
+ if (!cartItem.item)
+ return (
+ <CartItemStyles key={cartItem.id}>
+ <p>Ack! That Item is gone!</p>
+ <RemoveFromCart id={cartItem.id} />
+ </CartItemStyles>
+ );
+ return (
+ <CartItemStyles key={cartItem.id}>
+ <img width="100" src={cartItem.item.image} alt={cartItem.item.title} />
+ <div className="cart-item-details">
+ <h3>{cartItem.item.title}</h3>
+ <p>
+ {formatMoney(cartItem.quantity * cartItem.item.price)}
+ {' — '}
+ <em>
+ {cartItem.quantity} &times; {formatMoney(cartItem.item.price)} each
+ </em>
+ </p>
+ </div>
+ <RemoveFromCart id={cartItem.id} />
+ </CartItemStyles>
+ );
+};
+
+CartItem.propTypes = {
+ cartItem: PropTypes.object.isRequired,
+};
+
+export default CartItem;
diff --git a/finished-application/frontend/components/CreateItem.js b/finished-application/frontend/components/CreateItem.js
new file mode 100644
index 0000000..503b3cb
--- /dev/null
+++ b/finished-application/frontend/components/CreateItem.js
@@ -0,0 +1,148 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import Router from 'next/router';
+import wait from 'waait';
+import gql from 'graphql-tag';
+import Error from './ErrorMessage';
+import Form from './styles/Form';
+import formatMoney from '../lib/formatMoney';
+import { ALL_ITEMS_QUERY } from './Items';
+import { PAGINATION_QUERY } from './Pagination';
+
+const CREATE_ITEM_MUTATION = gql`
+ mutation CREATE_ITEM_MUTATION(
+ $description: String!
+ $title: String!
+ $price: Int!
+ $image: String
+ $largeImage: String
+ ) {
+ createItem(
+ description: $description
+ title: $title
+ price: $price
+ image: $image
+ largeImage: $largeImage
+ ) {
+ id
+ }
+ }
+`;
+
+class CreateItem extends Component {
+ state = {
+ title: '',
+ description: '',
+ image: '',
+ largeImage: '',
+ price: 0,
+ };
+
+ handleChange = e => {
+ const { name, value, type } = e.target;
+ const val = type === 'number' ? parseFloat(value) : value;
+ this.setState({ [name]: val });
+ };
+
+ uploadFile = async e => {
+ this.setState({ loading: true });
+ const files = e.currentTarget.files;
+ const data = new FormData();
+ data.append('file', files[0]);
+ data.append('upload_preset', 'sickfits');
+
+ // use the file endpoint
+ const res = await fetch('https://api.cloudinary.com/v1_1/wesbos/image/upload', {
+ method: 'POST',
+ body: data,
+ });
+ const file = await res.json();
+ this.setState({
+ image: file.secure_url,
+ largeImage: file.eager[0].secure_url,
+ loading: false,
+ });
+ };
+
+ render() {
+ return (
+ <Mutation
+ mutation={CREATE_ITEM_MUTATION}
+ variables={this.state}
+ refetchQueries={[{ query: ALL_ITEMS_QUERY }, { query: PAGINATION_QUERY }]}
+ >
+ {(createItem, { loading, error }) => (
+ <Form
+ data-test
+ onSubmit={async e => {
+ e.preventDefault();
+ const { data: { createItem: item } } = await createItem();
+ // we wait 0 ms so it puts the router push at the end of the call stack. This ensures that refetchQueries runs before we unmount the component :)
+ await wait();
+ Router.push({
+ pathname: `/item`,
+ query: { id: item.id },
+ });
+ }}
+ >
+ <h2>Sell an Item.</h2>
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="file">
+ Image
+ <input
+ required
+ id="file"
+ onChange={this.uploadFile}
+ type="file"
+ accept=".png, .jpg, .jpeg"
+ />
+ {this.state.image ? (
+ <img src={this.state.image} width="100" alt={this.state.title} />
+ ) : null}
+ </label>
+ <label htmlFor="title">
+ Title
+ <input
+ required
+ value={this.state.title}
+ onChange={this.handleChange}
+ type="text"
+ name="title"
+ id="title"
+ placeholder="Title"
+ />
+ </label>
+ <label htmlFor="price">
+ Price {this.state.price && formatMoney(this.state.price)}
+ <input
+ required
+ type="number"
+ id="price"
+ name="price"
+ min="0"
+ value={this.state.price}
+ onChange={this.handleChange}
+ />
+ </label>
+ <textarea
+ id="description"
+ required
+ name="description"
+ value={this.state.description}
+ onChange={this.handleChange}
+ placeholder="The desc for this item"
+ />
+ <button disabled={this.state.loading} type="submit">
+ Submit
+ </button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default CreateItem;
+export { CREATE_ITEM_MUTATION };
diff --git a/finished-application/frontend/components/DeleteItem.js b/finished-application/frontend/components/DeleteItem.js
new file mode 100644
index 0000000..4d68d5e
--- /dev/null
+++ b/finished-application/frontend/components/DeleteItem.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Mutation } from 'react-apollo';
+import PropTypes, { number } from 'prop-types';
+import gql from 'graphql-tag';
+import { withRouter } from 'next/router';
+import { ALL_ITEMS_QUERY } from './Items';
+import { PAGINATION_QUERY } from './Pagination';
+import { perPage } from '../config';
+
+const DELETE_ITEM_MUTATION = gql`
+ mutation deleteItem($id: ID!) {
+ deleteItem(id: $id) {
+ id
+ title
+ description
+ }
+ }
+`;
+
+class DeleteItem extends React.Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ };
+
+ update = (cache, payload) => {
+ const deletedItem = payload.data.deleteItem;
+ let { page = 1 } = this.props.router.query;
+ page = parseFloat(page);
+ const skip = page * perPage - perPage;
+ const variables = { skip };
+ const data = cache.readQuery({ query: ALL_ITEMS_QUERY, variables });
+ // filter this one out
+ data.items = data.items.filter(item => item.id !== deletedItem.id);
+ // write the data back to the cache
+ console.log(data.items);
+ cache.writeQuery({ query: ALL_ITEMS_QUERY, data, variables });
+ // FYI Pagination is broken with Apollo currently - will make a followup video
+ };
+
+ render() {
+ return (
+ <Mutation
+ mutation={DELETE_ITEM_MUTATION}
+ variables={{ id: this.props.id }}
+ refetchQueries={[
+ {
+ query: ALL_ITEMS_QUERY,
+ variables: { skip: (this.props.router.query.page || 1) * perPage - perPage },
+ },
+ { query: PAGINATION_QUERY },
+ ]}
+ update={this.update}
+ >
+ {(removeItem, { error }) => (
+ <button
+ onClick={() => {
+ if (confirm('Are you sure you want to delete this item?')) {
+ removeItem();
+ }
+ }}
+ >
+ {error ? error.message : '× Delete Item'}
+ </button>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default withRouter(DeleteItem);
+export { DELETE_ITEM_MUTATION };
diff --git a/finished-application/frontend/components/Dump.js b/finished-application/frontend/components/Dump.js
new file mode 100644
index 0000000..f281281
--- /dev/null
+++ b/finished-application/frontend/components/Dump.js
@@ -0,0 +1,19 @@
+const Dump = props => (
+ <div
+ style={{
+ fontSize: 20,
+ border: '1px solid #efefef',
+ padding: 10,
+ background: 'white',
+ }}
+ >
+ {Object.keys(props).map(prop => (
+ <pre key={prop}>
+ <strong style={{ color: 'white', background: 'red' }}>{prop} 💩</strong>
+ {JSON.stringify(props[prop], '', ' ')}
+ </pre>
+ ))}
+ </div>
+);
+
+export default Dump;
diff --git a/finished-application/frontend/components/EditUser.js b/finished-application/frontend/components/EditUser.js
new file mode 100644
index 0000000..1bb8e5b
--- /dev/null
+++ b/finished-application/frontend/components/EditUser.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import { Query, Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Form from './styles/Form';
+import { CURRENT_USER_QUERY } from './User';
+import Error from './ErrorMessage';
+import User from './User';
+
+const UPDATE_USER_MUTATION = gql`
+ mutation updateUser($name: String!) {
+ updateUser(name: $name) {
+ name
+ }
+ }
+`;
+
+class EditUser extends React.Component {
+ state = {
+ changes: {},
+ };
+
+ handleChange = e => {
+ const { name, value } = e.target;
+ const changes = {
+ ...this.state.changes,
+ [name]: value,
+ };
+ this.setState({ changes });
+ };
+
+ handleSubmit = async (e, updateUser) => {
+ e.preventDefault();
+ // only submit if there are real changes
+ if (Object.keys(this.state.changes).length === 0) return;
+ await updateUser();
+ this.setState({ changes: {} });
+ };
+
+ render() {
+ return (
+ <User>
+ {({ data: { me }, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ return (
+ <Mutation
+ mutation={UPDATE_USER_MUTATION}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ variables={this.state.changes}
+ >
+ {(updateUser, { error, called }) => (
+ <Form onSubmit={e => this.handleSubmit(e, updateUser)}>
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ {called && !error && <p data-test="updated">Updated!</p>}
+ <label htmlFor="name">
+ Name:
+ <input
+ type="text"
+ name="name"
+ defaultValue={me.name}
+ onChange={this.handleChange}
+ />
+ </label>
+ <button type="submit">Update</button>
+ <strong>me:</strong>
+ <pre>{JSON.stringify(me.name)}</pre>
+ <strong>Change:</strong>
+ <pre data-test="change">{JSON.stringify(this.state.changes)}</pre>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }}
+ </User>
+ );
+ }
+}
+
+export default EditUser;
+export { UPDATE_USER_MUTATION };
diff --git a/finished-application/frontend/components/ErrorMessage.js b/finished-application/frontend/components/ErrorMessage.js
new file mode 100644
index 0000000..52d59a8
--- /dev/null
+++ b/finished-application/frontend/components/ErrorMessage.js
@@ -0,0 +1,51 @@
+import styled from 'styled-components';
+import React from 'react';
+
+import PropTypes from 'prop-types';
+
+const ErrorStyles = styled.div`
+ padding: 2rem;
+ background: white;
+ margin: 2rem 0;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ border-left: 5px solid red;
+ p {
+ margin: 0;
+ font-weight: 100;
+ }
+ strong {
+ margin-right: 1rem;
+ }
+`;
+
+const DisplayError = ({ error }) => {
+ if (!error || !error.message) return null;
+ if (error.networkError && error.networkError.result && error.networkError.result.errors.length) {
+ return error.networkError.result.errors.map((error, i) => (
+ <ErrorStyles key={i}>
+ <p data-test="graphql-error">
+ <strong>Shoot!</strong>
+ {error.message.replace('GraphQL error: ', '')}
+ </p>
+ </ErrorStyles>
+ ));
+ }
+ return (
+ <ErrorStyles>
+ <p data-test="graphql-error">
+ <strong>Shoot!</strong>
+ {error.message.replace('GraphQL error: ', '')}
+ </p>
+ </ErrorStyles>
+ );
+};
+
+DisplayError.defaultProps = {
+ error: {},
+};
+
+DisplayError.propTypes = {
+ error: PropTypes.object,
+};
+
+export default DisplayError;
diff --git a/finished-application/frontend/components/Header.js b/finished-application/frontend/components/Header.js
new file mode 100644
index 0000000..bb05c34
--- /dev/null
+++ b/finished-application/frontend/components/Header.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import NProgress from 'nprogress';
+import Router from 'next/router';
+import styled from 'styled-components';
+import Link from 'next/link';
+import Cart from './Cart';
+import Search from './Search';
+import Nav from './Nav';
+
+Router.onRouteChangeStart = () => {
+ NProgress.start();
+};
+Router.onRouteChangeComplete = () => NProgress.done();
+Router.onRouteChangeError = () => NProgress.done();
+
+const StyledHeader = styled.header`
+ .bar {
+ border-bottom: 10px solid ${props => props.theme.black};
+ display: grid;
+ grid-template-columns: auto 1fr;
+ justify-content: space-between;
+ align-items: stretch;
+ @media (max-width: 1300px) {
+ grid-template-columns: 1fr;
+ justify-content: center;
+ }
+ }
+ .sub-bar {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ border-bottom: 1px solid ${props => props.theme.lightgrey};
+ }
+`;
+
+const Logo = styled.h1`
+ font-size: 4rem;
+ margin-left: 2rem;
+ position: relative;
+ z-index: 2;
+ transform: skew(-7deg);
+ a {
+ padding: 0.5rem 1rem;
+ background: ${props => props.theme.red};
+ color: white;
+ letter-spacing: -2px;
+ text-transform: uppercase;
+ }
+ @media (max-width: 1300px) {
+ margin: 0;
+ text-align: center;
+ }
+`;
+
+const Header = () => (
+ <StyledHeader>
+ <div className="bar">
+ <Logo>
+ <Link href="/">
+ <a>Sick&nbsp;Fits!</a>
+ </Link>
+ </Logo>
+ <Nav />
+ </div>
+ <div className="sub-bar">
+ <Search />
+ </div>
+ <Cart />
+ </StyledHeader>
+);
+
+export default Header;
diff --git a/finished-application/frontend/components/Item.js b/finished-application/frontend/components/Item.js
new file mode 100644
index 0000000..d6b339e
--- /dev/null
+++ b/finished-application/frontend/components/Item.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Link from 'next/link';
+import Title from './styles/Title';
+import AddToCart from './AddToCart';
+import DeleteItem from './DeleteItem';
+import formatMoney from '../lib/formatMoney';
+import ItemStyles from './styles/ItemStyles';
+import PriceTag from './styles/PriceTag';
+
+class Item extends React.Component {
+ static propTypes = {
+ item: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const item = this.props.item;
+ return (
+ <ItemStyles key={item.id}>
+ {item.image && <img src={item.image} alt={item.title} />}
+ <Title>
+ <Link
+ href={{
+ pathname: '/item',
+ query: { id: item.id },
+ }}
+ >
+ <a>{item.title}</a>
+ </Link>
+ </Title>
+
+ <PriceTag>{formatMoney(item.price)}</PriceTag>
+
+ <p>{item.description}</p>
+
+ <div className="buttonList">
+ <Link
+ href={{
+ pathname: '/update',
+ query: { id: item.id },
+ }}
+ >
+ <a>Edit ✏️</a>
+ </Link>
+ <AddToCart id={item.id} />
+ <DeleteItem id={item.id} />
+ </div>
+ </ItemStyles>
+ );
+ }
+}
+
+export default Item;
diff --git a/finished-application/frontend/components/Items.js b/finished-application/frontend/components/Items.js
new file mode 100644
index 0000000..5694d0b
--- /dev/null
+++ b/finished-application/frontend/components/Items.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Query } from 'react-apollo';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import Pagination from './Pagination';
+import Item from './Item';
+import LoadingItem from './LoadingItem';
+import { perPage } from '../config';
+
+const ALL_ITEMS_QUERY = gql`
+ query ALL_ITEMS_QUERY($skip: Int = 0, $first: Int = ${perPage}) {
+ items(orderBy: createdAt_DESC, first: $first, skip: $skip) {
+ __typename
+ id
+ title
+ price
+ description
+ image
+ largeImage
+ }
+ }
+`;
+
+const Items = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 60px;
+ max-width: ${props => props.theme.maxWidth};
+ margin: 0 auto;
+`;
+
+const Center = styled.div`
+ text-align: center;
+`;
+
+class ItemList extends React.Component {
+ static propTypes = {
+ page: PropTypes.number.isRequired,
+ };
+ render() {
+ return (
+ <Center key={this.props.page}>
+ <Pagination page={this.props.page} />
+ <Query
+ query={ALL_ITEMS_QUERY}
+ variables={{
+ skip: this.props.page * perPage - perPage,
+ first: perPage,
+ }}
+ // fetchPolicy="network-only"
+ >
+ {({ data, error, loading }) => {
+ if (loading) {
+ return (
+ <Items>
+ {Array.from({ length: 4 })
+ .map((x, id) => ({ id }))
+ .map(x => <LoadingItem key={x.id} />)}
+ </Items>
+ );
+ }
+ if (error) return <div>Error</div>;
+ return <Items>{data.items.map(item => <Item key={item.id} item={item} />)}</Items>;
+ }}
+ </Query>
+ <Pagination page={this.props.page} />
+ </Center>
+ );
+ }
+}
+
+export default ItemList;
+export { ALL_ITEMS_QUERY };
diff --git a/finished-application/frontend/components/LoadingItem.js b/finished-application/frontend/components/LoadingItem.js
new file mode 100644
index 0000000..5736337
--- /dev/null
+++ b/finished-application/frontend/components/LoadingItem.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import Title from './styles/Title';
+import ItemStyles from './styles/ItemStyles';
+
+class LoadingItem extends React.Component {
+ render() {
+ return (
+ <ItemStyles>
+ <img
+ src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
+ alt="Loading..."
+ />>
+ <Title>
+ <a>Loading...</a>
+ </Title>
+ <p>Please Wait</p>
+ </ItemStyles>
+ );
+ }
+}
+
+export default LoadingItem;
diff --git a/finished-application/frontend/components/Meta.js b/finished-application/frontend/components/Meta.js
new file mode 100644
index 0000000..fe9c29f
--- /dev/null
+++ b/finished-application/frontend/components/Meta.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import Head from 'next/head';
+
+const Meta = () => (
+ <div>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta charSet="utf-8" />
+ <link rel="shortcut icon" href="/static/favicon.png" />
+ <link rel="stylesheet" type="text/css" href="/static/nprogress.css" />
+ <title>Sick Fits!</title>
+ </Head>
+ </div>
+);
+
+export default Meta;
diff --git a/finished-application/frontend/components/Nav.js b/finished-application/frontend/components/Nav.js
new file mode 100644
index 0000000..b483376
--- /dev/null
+++ b/finished-application/frontend/components/Nav.js
@@ -0,0 +1,59 @@
+import React, { Fragment } from 'react';
+import Link from 'next/link';
+import { Query, Mutation } from 'react-apollo';
+import { TOGGLE_CART_MUTATION } from './Cart';
+import User from './User';
+import CartCount from './CartCount';
+import Signout from './Signout';
+import NavStyles from './styles/NavStyles';
+
+class Nav extends React.Component {
+ render() {
+ // below we set the fetchPolicy to network only so it forces re-fetch on the server
+ return (
+ <User>
+ {({ data: { me }, error }) => (
+ <NavStyles data-test="nav">
+ <Link href="/items">
+ <a>Shop</a>
+ </Link>
+ <Link href="/sell">
+ <a>Sell</a>
+ </Link>
+
+ {!me && (
+ <Link href="/signup">
+ <a>Sign In</a>
+ </Link>
+ )}
+
+ {me && (
+ <Fragment>
+ <Link href="/orders">
+ <a>Orders</a>
+ </Link>
+ <Link href="/me">
+ <a>My Account</a>
+ </Link>
+ <Signout />
+ <Mutation mutation={TOGGLE_CART_MUTATION}>
+ {toggleCart => (
+ <button onClick={toggleCart}>
+ My Cart
+ <CartCount
+ className="cart-count"
+ count={me.cart.reduce((tally, cartItem) => tally + cartItem.quantity, 0)}
+ />
+ </button>
+ )}
+ </Mutation>
+ </Fragment>
+ )}
+ </NavStyles>
+ )}
+ </User>
+ );
+ }
+}
+
+export default Nav;
diff --git a/finished-application/frontend/components/Order.js b/finished-application/frontend/components/Order.js
new file mode 100644
index 0000000..b4a0a0e
--- /dev/null
+++ b/finished-application/frontend/components/Order.js
@@ -0,0 +1,98 @@
+import { Component } from 'react';
+import { Query } from 'react-apollo';
+import { format } from 'date-fns';
+import Head from 'next/head';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import formatMoney from '../lib/formatMoney';
+import Error from './ErrorMessage';
+import OrderStyles from './styles/OrderStyles';
+
+const SINGLE_ORDER_QUERY = gql`
+ query SINGLE_ORDER_QUERY($id: ID!) {
+ order(id: $id) {
+ id
+ charge
+ total
+ createdAt
+ user {
+ id
+ }
+ items {
+ id
+ title
+ price
+ description
+ image
+ quantity
+ }
+ }
+ }
+`;
+
+class Order extends Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return (
+ <Query
+ query={SINGLE_ORDER_QUERY}
+ variables={{ id: this.props.id }}
+ fetchPolicy="network-only"
+ >
+ {({ data, error, loading, refetch }) => {
+ if (loading) return <p>Loading...</p>;
+ if (error) return <Error error={error} refetch={refetch} />;
+ const order = data.order;
+ return (
+ <OrderStyles data-test="order">
+ <Head>
+ <title>Sick Fits - Order {order.id}</title>
+ </Head>
+ <p>
+ <span>Order Id:</span>
+ <span>{order.id}</span>
+ </p>
+ <p>
+ <span>Charge</span>
+ <span>{order.charge}</span>
+ </p>
+ <p>
+ <span>Date</span>
+ <span>{format(order.createdAt, 'MMMM D, YYYY h:mm A')}</span>
+ </p>
+ <p>
+ <span>Order Total</span>
+ <span>{formatMoney(order.total)}</span>
+ </p>
+ <p>
+ <span>Item Count</span>
+ <span>{order.items.length}</span>
+ </p>
+ <div className="items">
+ {order.items.map(item => (
+ <div className="order-item" key={item.id}>
+ <img src={item.image} alt={item.title} />
+ <div className="item-details">
+ <h2> {item.title} </h2>
+ <p>Qty: {item.quantity}</p>
+ <p>Each: {formatMoney(item.price)}</p>
+ <p>Subtotal: {formatMoney(item.price * item.quantity)}</p>
+ <p>SubTotal: {item.description}</p>
+ <p>{item.description}</p>
+ </div>
+ </div>
+ ))}
+ </div>
+ </OrderStyles>
+ );
+ }}
+ </Query>
+ );
+ }
+}
+
+export default Order;
+export { SINGLE_ORDER_QUERY };
diff --git a/finished-application/frontend/components/OrderList.js b/finished-application/frontend/components/OrderList.js
new file mode 100644
index 0000000..5886ad7
--- /dev/null
+++ b/finished-application/frontend/components/OrderList.js
@@ -0,0 +1,93 @@
+import React from 'react';
+import { Query } from 'react-apollo';
+import { formatDistance } from 'date-fns';
+import Link from 'next/link';
+import styled from 'styled-components';
+import gql from 'graphql-tag';
+import formatMoney from '../lib/formatMoney';
+import OrderItemStyles from './styles/OrderItemStyles';
+
+const USER_ORDERS_QUERY = gql`
+ query orders {
+ orders(orderBy: createdAt_DESC) {
+ id
+ total
+ createdAt
+ updatedAt
+ items {
+ id
+ title
+ price
+ description
+ quantity
+ image
+ }
+ }
+ }
+`;
+
+const OrderUl = styled.ul`
+ display: grid;
+ grid-gap: 4rem;
+ grid-template-columns: repeat(auto-fit, minmax(40%, 1fr));
+`;
+
+class OrderList extends React.Component {
+ render() {
+ return (
+ <Query query={USER_ORDERS_QUERY}>
+ {({ data: { orders }, loading, error }) => {
+ if (loading) return <p>Loading...</p>;
+ if (error) return <p>Error...</p>;
+ if (!orders || !orders.length) return <p>No Orders Yet!</p>;
+ return (
+ <div>
+ <h2>You have {orders.length} Orders</h2>
+ <OrderUl>
+ {orders.map(order => (
+ <OrderItemStyles key={order.id}>
+ <Link
+ href={{
+ pathname: '/order',
+ query: { id: order.id },
+ }}
+ >
+ <a>
+ <div className="order-meta">
+ <p>
+ <strong>{order.items.reduce((a, b) => a + b.quantity, 0)}</strong>
+ Items
+ </p>
+ <p>
+ <strong>{order.items.length}</strong>
+ Products
+ </p>
+ <p>
+ <strong>{formatDistance(order.createdAt, new Date())}</strong>
+ ago
+ </p>
+ <p>
+ <strong>{formatMoney(order.total)}</strong>
+ Total
+ </p>
+ </div>
+ <div className="images">
+ {order.items.map(item => (
+ <img key={item.id} src={item.image} alt={item.title} />
+ ))}
+ </div>
+ </a>
+ </Link>
+ </OrderItemStyles>
+ ))}
+ </OrderUl>
+ </div>
+ );
+ }}
+ </Query>
+ );
+ }
+}
+
+export default OrderList;
+export { USER_ORDERS_QUERY };
diff --git a/finished-application/frontend/components/Page.js b/finished-application/frontend/components/Page.js
new file mode 100644
index 0000000..cc0bc18
--- /dev/null
+++ b/finished-application/frontend/components/Page.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import styled, { ThemeProvider, injectGlobal } from 'styled-components';
+import PropTypes from 'prop-types';
+import Header from './Header';
+import Meta from './Meta';
+
+const theme = {
+ red: '#FF0000',
+ black: '#393939',
+ grey: '#3A3A3A',
+ lightgrey: '#E1E1E1',
+ offWhite: '#EDEDED',
+ maxWidth: '1300px',
+ bs: '0 12px 24px 0 rgba(0, 0, 0, 0.09)',
+};
+
+injectGlobal`
+ html {
+ box-sizing: border-box;
+ font-size: 10px;
+ }
+ body {
+ font-family: 'radnika next', sans-serif;
+ padding: 0;
+ background-color: #ffffff;
+ margin: 0;
+ font-size: 1.5rem;
+ line-height: 2;
+ }
+ *, *:before, *:after {
+ box-sizing: inherit;
+ }
+ a {
+ color: ${theme.black};
+ text-decoration: none;
+ }
+`;
+
+const Inner = styled.div`
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 2rem;
+`;
+
+const StyledPage = styled.div`
+ color: ${props => props.theme.black};
+ background: white;
+`;
+
+class Page extends React.Component {
+ static propTypes = {
+ children: PropTypes.node.isRequired,
+ };
+ render() {
+ return (
+ <ThemeProvider theme={theme}>
+ <StyledPage className="main">
+ <Meta />
+ <Header />
+ <Inner>{this.props.children}</Inner>
+ </StyledPage>
+ </ThemeProvider>
+ );
+ }
+}
+
+export default Page;
diff --git a/finished-application/frontend/components/Pagination.js b/finished-application/frontend/components/Pagination.js
new file mode 100644
index 0000000..b77f8f6
--- /dev/null
+++ b/finished-application/frontend/components/Pagination.js
@@ -0,0 +1,97 @@
+import React from 'react';
+import { Query } from 'react-apollo';
+import styled from 'styled-components';
+import Link from 'next/link';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import Head from 'next/head';
+import { perPage } from '../config';
+
+const PAGINATION_QUERY = gql`
+ query itemsConnection($skip: Int = 0, $first: Int = 4) {
+ itemsConnection(orderBy: createdAt_DESC, first: $first, skip: $skip) {
+ aggregate {
+ count
+ }
+ }
+ }
+`;
+
+const PaginationStyles = styled.div`
+ text-align: center;
+ display: inline-grid;
+ grid-template-columns: repeat(4, auto);
+ align-items: stretch;
+ justify-content: center;
+ align-content: center;
+ margin: 2rem 0;
+ border: 1px solid ${props => props.theme.lightgrey};
+ border-radius: 10px;
+ & > * {
+ margin: 0;
+ padding: 15px 30px;
+ border-right: 1px solid ${props => props.theme.lightgrey};
+ &:last-child {
+ border-right: 0;
+ }
+ }
+ a[aria-disabled='true'] {
+ color: grey;
+ pointer-events: none;
+ }
+`;
+
+const Pagination = props => (
+ <Query query={PAGINATION_QUERY}>
+ {({ data, loading, error }) => {
+ if (loading || error) return null;
+ const { aggregate } = data.itemsConnection;
+ const { page } = props;
+ const pages = Math.ceil(aggregate.count / perPage);
+ return (
+ <PaginationStyles data-test="pagination">
+ <Head>
+ <title>
+ Sick Fits! — Page {page} of {pages}
+ </title>
+ </Head>
+ <Link
+ prefetch
+ href={{
+ pathname: 'items',
+ query: { page: page - 1 },
+ }}
+ >
+ <a className="prev" aria-disabled={page <= 1}>
+ ←Prev
+ </a>
+ </Link>
+ <p>
+ Page <strong>{page} </strong> of <strong className="totalPages">{pages}</strong>
+ </p>
+ <p>
+ <strong>{aggregate.count}</strong> Items Total
+ </p>
+ <Link
+ prefetch
+ href={{
+ pathname: 'items',
+ query: { page: page + 1 },
+ }}
+ >
+ <a className="next" aria-disabled={page >= pages}>
+ Next →
+ </a>
+ </Link>
+ </PaginationStyles>
+ );
+ }}
+ </Query>
+);
+
+Pagination.propTypes = {
+ page: PropTypes.number.isRequired,
+};
+
+export default Pagination;
+export { PAGINATION_QUERY };
diff --git a/finished-application/frontend/components/Permissions.js b/finished-application/frontend/components/Permissions.js
new file mode 100644
index 0000000..52a6acf
--- /dev/null
+++ b/finished-application/frontend/components/Permissions.js
@@ -0,0 +1,133 @@
+import React from 'react';
+import { Query, Mutation } from 'react-apollo';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import Error from './ErrorMessage';
+import SickButton from './styles/SickButton';
+import Table from './styles/Table';
+
+const ALL_USERS_QUERY = gql`
+ query {
+ users {
+ id
+ name
+ email
+ permissions
+ }
+ }
+`;
+
+const UPDATE_PERMISSIONS_MUTATION = gql`
+ mutation updatePermissions($permissions: [Permission], $userId: ID!) {
+ updatePermissions(permissions: $permissions, userId: $userId) {
+ id
+ permissions
+ name
+ email
+ }
+ }
+`;
+
+const possiblePermissions = [
+ 'ADMIN',
+ 'USER',
+ 'ITEMCREATE',
+ 'ITEMUPDATE',
+ 'ITEMDELETE',
+ 'PERMISSIONUPDATE',
+];
+
+class User extends React.Component {
+ static propTypes = {
+ user: PropTypes.shape({
+ permissions: PropTypes.array.isRequired,
+ id: PropTypes.string.isRequired,
+ }).isRequired,
+ };
+ state = {
+ permissions: this.props.user.permissions,
+ };
+ handlePermissionsChange = e => {
+ const checkbox = e.target;
+ let updatedPermissions = [...this.state.permissions];
+ if (checkbox.checked) {
+ // add it in
+ updatedPermissions.push(checkbox.value);
+ } else {
+ updatedPermissions = updatedPermissions.filter(permission => permission !== checkbox.value);
+ }
+ this.setState({ permissions: updatedPermissions });
+ };
+ render() {
+ const { user } = this.props;
+ return (
+ <Mutation mutation={UPDATE_PERMISSIONS_MUTATION}>
+ {(updatePermissions, { loading, error }) => (
+ <tr key={user.id} className="user">
+ <Error error={error} />
+ <td>{user.name}</td>
+ <td>{user.email}</td>
+ {possiblePermissions.map(permission => (
+ <td>
+ <label key={permission} htmlFor={`${user.id}-permission-${permission}`}>
+ <input
+ type="checkbox"
+ checked={this.state.permissions.includes(permission)}
+ name={`permission-${permission}`}
+ id={`${user.id}-permission-${permission}`}
+ onChange={this.handlePermissionsChange}
+ value={permission}
+ />
+ </label>
+ </td>
+ ))}
+ <td>
+ <SickButton
+ type="button"
+ disabled={loading}
+ onClick={async () => {
+ await updatePermissions({
+ variables: {
+ permissions: this.state.permissions,
+ userId: this.props.user.id,
+ },
+ });
+ }}
+ >
+ Updat{loading ? 'ing' : 'e'}
+ </SickButton>
+ </td>
+ </tr>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+const Permissions = () => (
+ <Query query={ALL_USERS_QUERY}>
+ {({ data, error, loading }) => {
+ if (loading) return <div>Loading</div>;
+ if (error) return <Error error={error} />;
+ return (
+ <div>
+ <h1>Manage User Permissions</h1>
+ <Table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Email</th>
+ {possiblePermissions.map(p => <th>{p}</th>)}
+ <th>👇🏻</th>
+ </tr>
+ </thead>
+ <tbody>{data.users.map(user => <User key={user.id} user={user} />)}</tbody>
+ </Table>
+ </div>
+ );
+ }}
+ </Query>
+);
+
+export default Permissions;
+export { ALL_USERS_QUERY, UPDATE_PERMISSIONS_MUTATION };
diff --git a/finished-application/frontend/components/PleaseSignIn.js b/finished-application/frontend/components/PleaseSignIn.js
new file mode 100644
index 0000000..5646749
--- /dev/null
+++ b/finished-application/frontend/components/PleaseSignIn.js
@@ -0,0 +1,46 @@
+import { Query } from 'react-apollo';
+import PropTypes from 'prop-types';
+import { CURRENT_USER_QUERY } from './User';
+import Signin from './Signin';
+
+const PleaseSignIn = props => (
+ <Query query={CURRENT_USER_QUERY}>
+ {({ data, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ // check if they are signed in
+ if (!data.me) {
+ return (
+ <div>
+ <p>Please sign in before continuing!</p>
+ <Signin />
+ </div>
+ );
+ }
+ // check if they need permissions
+ if (props.allowedPermissions) {
+ // check if they NO permissions, or they don't meet the requmrenets
+ if (
+ !data.me.permissions ||
+ !props.allowedPermissions.some(permission => data.me.permissions.includes(permission))
+ ) {
+ return (
+ <p>
+ Insufficient Permissions. You have:
+ <strong>{data.me.permissions}</strong>
+ and you need
+ <strong>{props.allowedPermissions.join(' OR ')}</strong>
+ </p>
+ );
+ }
+ }
+ return props.children;
+ }}
+ </Query>
+);
+
+PleaseSignIn.propTypes = {
+ allowedPermissions: PropTypes.array,
+ children: PropTypes.any.isRequired,
+};
+
+export default PleaseSignIn;
diff --git a/finished-application/frontend/components/RemoveFromCart.js b/finished-application/frontend/components/RemoveFromCart.js
new file mode 100644
index 0000000..d29cbf1
--- /dev/null
+++ b/finished-application/frontend/components/RemoveFromCart.js
@@ -0,0 +1,57 @@
+import { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+
+const REMOVE_FROM_CART_MUTATION = gql`
+ mutation removeFromCart($id: ID!) {
+ removeFromCart(id: $id) {
+ id
+ }
+ }
+`;
+
+const BigButton = styled.button`
+ font-size: 3rem;
+ background: none;
+ border: 0;
+ &:hover {
+ color: ${props => props.theme.red};
+ cursor: pointer;
+ }
+`;
+
+class RemoveFromCart extends Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ };
+
+ update = (cache, payload) => {
+ const data = cache.readQuery({ query: CURRENT_USER_QUERY });
+ // console.log(data.me.cart[0]);
+ const cartItemId = payload.data.removeFromCart.id;
+ data.me.cart = data.me.cart.filter(cartItem => cartItem.id !== cartItemId);
+ cache.writeQuery({ query: CURRENT_USER_QUERY, data });
+ };
+
+ render() {
+ return (
+ <Mutation
+ mutation={REMOVE_FROM_CART_MUTATION}
+ variables={{ id: this.props.id }}
+ update={this.update}
+ >
+ {(removeFromCart, { loading }) => (
+ <BigButton disabled={loading} title="Remove From Cart" onClick={() => removeFromCart}>
+ ×
+ </BigButton>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default RemoveFromCart;
+export { REMOVE_FROM_CART_MUTATION };
diff --git a/finished-application/frontend/components/Reset.js b/finished-application/frontend/components/Reset.js
new file mode 100644
index 0000000..bb2ece0
--- /dev/null
+++ b/finished-application/frontend/components/Reset.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import { Mutation } from 'react-apollo';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+import { CURRENT_USER_QUERY } from './User';
+
+const RESET_MUTATION = gql`
+ mutation RESET_MUTATION($resetToken: String!, $password: String!, $confirmPassword: String!) {
+ resetPassword(resetToken: $resetToken, password: $password, confirmPassword: $confirmPassword) {
+ id
+ email
+ name
+ }
+ }
+`;
+
+class Reset extends React.Component {
+ static propTypes = {
+ resetToken: PropTypes.string.isRequired,
+ };
+
+ state = {
+ confirmPassword: '',
+ password: '',
+ };
+
+ saveToState = e => {
+ const { name, value } = e.target;
+ this.setState({ [name]: value });
+ };
+
+ resetPassword = async (e, resetMutation) => {
+ e.preventDefault();
+ await resetMutation();
+ };
+
+ render() {
+ return (
+ <Mutation
+ mutation={RESET_MUTATION}
+ variables={{
+ resetToken: this.props.resetToken,
+ password: this.state.password,
+ confirmPassword: this.state.confirmPassword,
+ }}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {(resetMutation, { error, loading }) => (
+ <Form onSubmit={e => this.resetPassword(e, resetMutation)}>
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="password">
+ password
+ <input
+ value={this.state.password}
+ onChange={this.saveToState}
+ name="password"
+ type="password"
+ id="password"
+ />
+ </label>
+ <label htmlFor="confirm">
+ Confirm:
+ <input
+ value={this.state.confirmPassword}
+ onChange={this.saveToState}
+ name="confirmPassword"
+ type="password"
+ id="confirmPassword"
+ />
+ </label>
+
+ <button type="submit">Request Reset!</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default Reset;
+export { RESET_MUTATION };
diff --git a/finished-application/frontend/components/ResetRequest.js b/finished-application/frontend/components/ResetRequest.js
new file mode 100644
index 0000000..6aedcc1
--- /dev/null
+++ b/finished-application/frontend/components/ResetRequest.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+
+const REQUEST_RESET_MUTATION = gql`
+ mutation requestReset($email: String!) {
+ requestReset(email: $email) {
+ id
+ }
+ }
+`;
+
+class ResetRequest extends React.Component {
+ state = {
+ email: '',
+ };
+
+ render() {
+ return (
+ <Mutation mutation={REQUEST_RESET_MUTATION} variables={this.state}>
+ {(resetMutation, { loading, error, called }) => (
+ <Form
+ onSubmit={async e => {
+ e.preventDefault();
+ await resetMutation();
+ }}
+ data-test="ResetRequest"
+ >
+ <Error error={error} />
+ {!error && called && !loading && <p>Success! Check Your Email!</p>}
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="email">
+ Email
+ <input
+ value={this.state.email}
+ onChange={e => this.setState({ email: e.target.value })}
+ name="email"
+ type="text"
+ placeholder="email"
+ />
+ </label>
+
+ <button type="submit">Request Reset!</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default ResetRequest;
+export { REQUEST_RESET_MUTATION };
diff --git a/finished-application/frontend/components/Search.js b/finished-application/frontend/components/Search.js
new file mode 100644
index 0000000..a972db2
--- /dev/null
+++ b/finished-application/frontend/components/Search.js
@@ -0,0 +1,143 @@
+import React from 'react';
+import Downshift from 'downshift';
+import Router from 'next/router';
+import { ApolloConsumer } from 'react-apollo';
+import gql from 'graphql-tag';
+import styled, { keyframes } from 'styled-components';
+import debounce from 'lodash.debounce';
+
+const SEARCH_ITEMS_QUERY = gql`
+ query SEARCH_ITEMS_QUERY($searchTerm: String!) {
+ items(where: { OR: [{ title_contains: $searchTerm }, { description_contains: $searchTerm }] }) {
+ id
+ image
+ title
+ }
+ }
+`;
+function routeToItem(item) {
+ Router.push({
+ pathname: '/item',
+ query: {
+ id: item.id,
+ },
+ });
+}
+
+const DropDown = styled.div`
+ position: absolute;
+ width: 100%;
+ z-index: 2;
+ border: 1px solid ${props => props.theme.lightgrey};
+`;
+
+const DropDownItem = styled.div`
+ border-bottom: 1px solid ${props => props.theme.lightgrey};
+ background: ${props => (props.highlighted ? '#f7f7f7' : 'white')};
+ padding: 1rem;
+ transition: all 0.2s;
+ ${props => (props.highlighted ? 'padding-left: 2rem;' : null)};
+ display: flex;
+ align-items: center;
+ border-left: 10px solid ${props => (props.highlighted ? props.theme.lightgrey : 'white')};
+ img {
+ margin-right: 10px;
+ }
+`;
+
+const glow = keyframes`
+ from {
+ box-shadow: 0 0 0px yellow;
+ }
+
+ to {
+ box-shadow: 0 0 10px 1px yellow;
+ }
+`;
+
+const SearchStyles = styled.div`
+ position: relative;
+ input {
+ width: 100%;
+ padding: 10px;
+ border: 0;
+ font-size: 2rem;
+ &.loading {
+ animation: ${glow} 0.5s ease-in-out infinite alternate;
+ }
+ }
+`;
+
+class AutoComplete extends React.Component {
+ state = {
+ items: [],
+ loading: false,
+ };
+ onChange = async (e, client) => {
+ if (!e.target.value) {
+ return this.setState({ items: [] });
+ }
+ this.setState({ loading: true });
+ this.search(client, e.target.value);
+ };
+
+ search = debounce(async (client, searchTerm) => {
+ const res = await client.query({
+ query: SEARCH_ITEMS_QUERY,
+ variables: { searchTerm },
+ });
+ this.setState({ items: res.data.items, loading: false });
+ }, 350);
+
+ render() {
+ return (
+ <SearchStyles>
+ <Downshift onChange={routeToItem} itemToString={i => (i === null ? '' : i.title)}>
+ {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex }) => (
+ <div>
+ {/* This is the searchInput */}
+ <ApolloConsumer>
+ {client => (
+ <input
+ {...getInputProps({
+ placeholder: 'Search For Item',
+ id: 'search',
+ className: this.state.loading ? 'loading' : '',
+ onChange: e => {
+ e.persist();
+ this.onChange(e, client);
+ },
+ })}
+ />
+ )}
+ </ApolloConsumer>
+ {/* This is the Dropdown */}
+ {isOpen && (
+ <DropDown>
+ {this.state.items.map((item, index) => (
+ <DropDownItem
+ {...getItemProps({ item })}
+ key={item.id}
+ highlighted={highlightedIndex === index}
+ >
+ <img width="50" src={item.image} alt={item.title} />
+ {item.title}
+ </DropDownItem>
+ ))}
+ {/* Found Nothing State */}
+ {!this.state.items.length &&
+ !this.state.loading && (
+ <DropDownItem>Nothing Found for {inputValue}...</DropDownItem>
+ )}
+ </DropDown>
+ )}
+ </div>
+ )}
+ </Downshift>
+ </SearchStyles>
+ );
+ }
+}
+
+export default AutoComplete;
+export { SEARCH_ITEMS_QUERY };
diff --git a/finished-application/frontend/components/Signin.js b/finished-application/frontend/components/Signin.js
new file mode 100644
index 0000000..53cdb76
--- /dev/null
+++ b/finished-application/frontend/components/Signin.js
@@ -0,0 +1,79 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+import Error from './ErrorMessage';
+import Form from './styles/Form';
+
+const SIGNIN_MUTATION = gql`
+ mutation SIGNIN_MUTATION($email: String!, $password: String!) {
+ signin(email: $email, password: $password) {
+ id
+ email
+ name
+ }
+ }
+`;
+
+class Signin extends Component {
+ state = {
+ email: '',
+ password: '',
+ };
+
+ saveToState = e => {
+ const { name, value } = e.target;
+ this.setState({ [name]: value });
+ };
+
+ render() {
+ return (
+ <Mutation
+ mutation={SIGNIN_MUTATION}
+ variables={this.state}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {(signin, { loading, error }) => (
+ <Form
+ onSubmit={e => {
+ e.preventDefault();
+ signin();
+ }}
+ >
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="email">
+ Email
+ <input
+ value={this.state.email}
+ onChange={this.saveToState}
+ name="email"
+ type="text"
+ placeholder="email"
+ />
+ </label>
+
+ <label htmlFor="password">
+ Password
+ <input
+ type="password"
+ name="password"
+ id="password"
+ className="password"
+ placeholder="password"
+ value={this.state.password}
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <button type="submit">Sign In!</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default Signin;
+export { SIGNIN_MUTATION };
diff --git a/finished-application/frontend/components/Signout.js b/finished-application/frontend/components/Signout.js
new file mode 100644
index 0000000..ae87631
--- /dev/null
+++ b/finished-application/frontend/components/Signout.js
@@ -0,0 +1,25 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+
+const SIGN_OUT_MUTATION = gql`
+ mutation SIGN_OUT_MUTATION {
+ signout {
+ message
+ }
+ }
+`;
+
+class Signout extends Component {
+ render() {
+ return (
+ <Mutation mutation={SIGN_OUT_MUTATION} refetchQueries={[{ query: CURRENT_USER_QUERY }]}>
+ {signout => <button onClick={signout}>Sign Out</button>}
+ </Mutation>
+ );
+ }
+}
+
+export default Signout;
+export { SIGN_OUT_MUTATION };
diff --git a/finished-application/frontend/components/Signup.js b/finished-application/frontend/components/Signup.js
new file mode 100644
index 0000000..9164e67
--- /dev/null
+++ b/finished-application/frontend/components/Signup.js
@@ -0,0 +1,93 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { CURRENT_USER_QUERY } from './User';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+
+const SIGNUP_MUTATION = gql`
+ mutation SIGNUP_MUTATION($email: String!, $name: String!, $password: String!) {
+ signup(email: $email, name: $name, password: $password) {
+ id
+ email
+ name
+ }
+ }
+`;
+
+class Signup extends Component {
+ state = {
+ email: '',
+ name: '',
+ password: '',
+ };
+
+ saveToState = e => {
+ const { name, value } = e.target;
+ this.setState({ [name]: value });
+ };
+
+ render() {
+ return (
+ <Mutation
+ mutation={SIGNUP_MUTATION}
+ variables={this.state}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {(signup, { loading, error }) => (
+ <Form
+ method="post"
+ onSubmit={async e => {
+ e.preventDefault();
+ const res = await signup();
+ }}
+ >
+ <fieldset disabled={loading} aria-busy={loading}>
+ <Error error={error} />
+ <h2>Sign Up for an Account</h2>
+ <label htmlFor="email">
+ Email
+ <input
+ value={this.state.email}
+ onChange={this.saveToState}
+ name="email"
+ type="text"
+ placeholder="email"
+ />
+ </label>
+
+ <label htmlFor="name">
+ Name
+ <input
+ type="text"
+ name="name"
+ placeholder="name"
+ value={this.state.name}
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <label htmlFor="signupPassword">
+ Password
+ <input
+ type="password"
+ name="password"
+ id="signupPassword"
+ className="password"
+ placeholder="password"
+ value={this.state.password}
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <button type="submit">Submit</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default Signup;
+export { SIGNUP_MUTATION };
diff --git a/finished-application/frontend/components/SingleItem.js b/finished-application/frontend/components/SingleItem.js
new file mode 100644
index 0000000..83ef8a0
--- /dev/null
+++ b/finished-application/frontend/components/SingleItem.js
@@ -0,0 +1,77 @@
+import { Query } from 'react-apollo';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import Head from 'next/head';
+import Link from 'next/link';
+import gql from 'graphql-tag';
+import Error from './ErrorMessage';
+import AddToCart from './AddToCart';
+
+const SINGLE_ITEM_QUERY = gql`
+ query SINGLE_ITEM_QUERY($id: ID!) {
+ items(where: { id: $id }) {
+ id
+ title
+ description
+ largeImage
+ image
+ }
+ }
+`;
+
+const SingleItemStyles = styled.div`
+ max-width: 1200px;
+ margin: 2rem auto;
+ box-shadow: ${props => props.theme.bs};
+ display: grid;
+ grid-auto-columns: 1fr;
+ grid-auto-flow: column;
+ min-height: 800px;
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+ .details {
+ margin: 3rem;
+ font-size: 2rem;
+ }
+`;
+
+const SingleItem = props => (
+ <Query query={SINGLE_ITEM_QUERY} variables={{ id: props.id }}>
+ {({ data, loading, error }) => {
+ if (loading) return <p>Loading...</p>;
+ if (error) return <Error error={error} />;
+ const [item] = data.items;
+ return (
+ <SingleItemStyles data-test="SingleItem">
+ <Head>
+ <title>{item.title}</title>
+ </Head>
+ <img src={item.largeImage || item.image} alt={item.title} />
+ <div className="details">
+ <h2>Viewing {item.title}</h2>
+ <p>{item.description}</p>
+ <Link
+ href={{
+ pathname: '/update',
+ query: { id: item.id },
+ }}
+ >
+ <a>Edit ✏️</a>
+ </Link>
+ <AddToCart id={item.id} />
+ </div>
+ </SingleItemStyles>
+ );
+ }}
+ </Query>
+);
+
+SingleItem.propTypes = {
+ id: PropTypes.string.isRequired,
+};
+
+export default SingleItem;
+export { SINGLE_ITEM_QUERY };
diff --git a/finished-application/frontend/components/TakeMyMoney.js b/finished-application/frontend/components/TakeMyMoney.js
new file mode 100644
index 0000000..b9e42ea
--- /dev/null
+++ b/finished-application/frontend/components/TakeMyMoney.js
@@ -0,0 +1,83 @@
+import { Component } from 'react';
+import StripeCheckout from 'react-stripe-checkout';
+import { Mutation, Query } from 'react-apollo';
+import Router from 'next/router';
+import NProgress from 'nprogress';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import calcTotalPrice from '../lib/calcTotalPrice';
+import Error from './ErrorMessage';
+import User, { CURRENT_USER_QUERY } from './User';
+
+const CREATE_ORDER_MUTATION = gql`
+ mutation createOrder($token: String!) {
+ createOrder(token: $token) {
+ id
+ charge
+ total
+ items {
+ id
+ title
+ }
+ }
+ }
+`;
+
+function totalItems(cart) {
+ return cart.reduce((tally, cartItem) => tally + cartItem.quantity, 0);
+}
+
+class TakeMyMoney extends Component {
+ onToken = async (res, createOrder) => {
+ NProgress.start();
+ const order = await createOrder({
+ variables: {
+ token: res.id,
+ },
+ });
+ console.log(order);
+ // Route them to that order page
+ const { id } = order.data.createOrder;
+ Router.push({
+ pathname: `/order`,
+ query: { id },
+ });
+ };
+ render() {
+ return (
+ <User>
+ {({ data: { me }, error }) => {
+ if (!me || !me.cart.length) return null;
+ if (error) return <Error error={error} />;
+ return (
+ <Mutation
+ mutation={CREATE_ORDER_MUTATION}
+ refetchQueries={[{ query: CURRENT_USER_QUERY }]}
+ >
+ {createOrder => (
+ <StripeCheckout
+ amount={calcTotalPrice(me.cart)}
+ name="Sick Fits Haul"
+ description={`Order of ${totalItems(me.cart)} Items From Sick Fits`}
+ image={me.cart[0].item && me.cart[0].item.image}
+ token={res => this.onToken(res, createOrder)}
+ stripeKey="pk_lclTtThFp8CnO3QtEZSd8HA9mFUps"
+ currency="USD"
+ email={me.email}
+ >
+ {this.props.children}
+ </StripeCheckout>
+ )}
+ </Mutation>
+ );
+ }}
+ </User>
+ );
+ }
+}
+
+TakeMyMoney.propTypes = {
+ children: PropTypes.any,
+};
+
+export default TakeMyMoney;
diff --git a/finished-application/frontend/components/UpdateItem.js b/finished-application/frontend/components/UpdateItem.js
new file mode 100644
index 0000000..08ba254
--- /dev/null
+++ b/finished-application/frontend/components/UpdateItem.js
@@ -0,0 +1,105 @@
+import React, { Component } from 'react';
+import { Query, Mutation } from 'react-apollo';
+import PropTypes from 'prop-types';
+import gql from 'graphql-tag';
+import { SINGLE_ITEM_QUERY } from './SingleItem';
+import Form from './styles/Form';
+import Error from './ErrorMessage';
+
+const UPDATE_ITEM_MUTATION = gql`
+ mutation updateItem($id: ID!, $title: String, $description: String, $price: Int) {
+ updateItem(id: $id, description: $description, title: $title, price: $price) {
+ id
+ }
+ }
+`;
+
+class UpdateItem extends Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ };
+ state = {
+ item: {},
+ };
+
+ saveToState = e => {
+ let { name, value, type } = e.target;
+ if (type === 'number') {
+ value = parseInt(value);
+ }
+ const item = { ...this.state.item };
+ item[name] = value;
+ this.setState({ item });
+ };
+
+ updateItem = async (e, updateItemMutation) => {
+ console.log('Updating Item');
+ e.preventDefault();
+
+ const res = await updateItemMutation({
+ // pass in those variables from state
+ variables: {
+ id: this.props.id,
+ ...this.state.item,
+ },
+ });
+ console.log(res);
+ };
+
+ render() {
+ return (
+ <Query query={SINGLE_ITEM_QUERY} variables={{ id: this.props.id }}>
+ {({ data: { items }, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ if (!items || !items.length) return <p>Item Not Found</p>;
+ const [item] = items;
+ return (
+ <Mutation mutation={UPDATE_ITEM_MUTATION}>
+ {(updateItemMutation, { error }) => (
+ <Form onSubmit={e => this.updateItem(e, updateItemMutation)}>
+ <Error error={error} />
+ <h2>Edit {item.title}</h2>
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="title">
+ Title
+ <input
+ id="title"
+ defaultValue={item.title}
+ name="title"
+ onChange={this.saveToState}
+ type="text"
+ />
+ </label>
+
+ <label htmlFor="description">
+ Description
+ <textarea
+ defaultValue={item.description}
+ name="description"
+ onChange={this.saveToState}
+ />
+ </label>
+
+ <label htmlFor="price">
+ Price
+ <input
+ type="number"
+ name="price"
+ onChange={this.saveToState}
+ defaultValue={item.price}
+ />
+ </label>
+ <button type="submit">Save...</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }}
+ </Query>
+ );
+ }
+}
+
+export default UpdateItem;
+export { UPDATE_ITEM_MUTATION };
diff --git a/finished-application/frontend/components/User.js b/finished-application/frontend/components/User.js
new file mode 100644
index 0000000..19c568d
--- /dev/null
+++ b/finished-application/frontend/components/User.js
@@ -0,0 +1,45 @@
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import PropTypes from 'prop-types';
+
+const CURRENT_USER_QUERY = gql`
+ query {
+ me {
+ id
+ email
+ name
+ permissions
+ orders {
+ id
+ charge
+ total
+ }
+ cart {
+ id
+ quantity
+ item {
+ __typename
+ id
+ title
+ price
+ description
+ image
+ largeImage
+ }
+ }
+ }
+ }
+`;
+
+const User = props => (
+ <Query {...props} query={CURRENT_USER_QUERY}>
+ {result => props.children(result)}
+ </Query>
+);
+
+User.propTypes = {
+ children: PropTypes.func.isRequired,
+};
+
+export default User;
+export { CURRENT_USER_QUERY };
diff --git a/finished-application/frontend/components/styles/CartStyles.js b/finished-application/frontend/components/styles/CartStyles.js
new file mode 100644
index 0000000..d5ee93a
--- /dev/null
+++ b/finished-application/frontend/components/styles/CartStyles.js
@@ -0,0 +1,47 @@
+import styled from 'styled-components';
+
+const CartStyles = styled.div`
+ padding: 20px;
+ position: relative;
+ background: white;
+ position: fixed;
+ height: 100%;
+ top: 0;
+ right: 0;
+ width: 40%;
+ min-width: 500px;
+ bottom: 0;
+ transform: translateX(100%);
+ transition: all 0.3s;
+ box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2);
+ z-index: 5;
+ display: grid;
+ grid-template-rows: auto 1fr auto;
+ ${props => props.open && `transform: translateX(0);`};
+ header {
+ border-bottom: 5px solid ${props => props.theme.black};
+ margin-bottom: 2rem;
+ padding-bottom: 2rem;
+ }
+ footer {
+ border-top: 10px double ${props => props.theme.black};
+ margin-top: 2rem;
+ padding-top: 2rem;
+ display: grid;
+ grid-template-columns: auto auto;
+ align-items: center;
+ font-size: 3rem;
+ font-weight: 900;
+ p {
+ margin: 0;
+ }
+ }
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ overflow: scroll;
+ }
+`;
+
+export default CartStyles;
diff --git a/finished-application/frontend/components/styles/CloseButton.js b/finished-application/frontend/components/styles/CloseButton.js
new file mode 100644
index 0000000..69fd55c
--- /dev/null
+++ b/finished-application/frontend/components/styles/CloseButton.js
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+const CloseButton = styled.button`
+ background: black;
+ color: white;
+ font-size: 3rem;
+ border: 0;
+ position: absolute;
+ z-index: 2;
+ right: 0;
+`;
+
+export default CloseButton;
diff --git a/finished-application/frontend/components/styles/Form.js b/finished-application/frontend/components/styles/Form.js
new file mode 100644
index 0000000..5717130
--- /dev/null
+++ b/finished-application/frontend/components/styles/Form.js
@@ -0,0 +1,71 @@
+import styled, { keyframes } from 'styled-components';
+
+const loading = keyframes`
+ from {
+ background-position: 0 0;
+ /* rotate: 0; */
+ }
+
+ to {
+ background-position: 100% 100%;
+ /* rotate: 360deg; */
+ }
+`;
+
+const Form = styled.form`
+ box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.05);
+ background: rgba(0, 0, 0, 0.02);
+ border: 5px solid white;
+ padding: 20px;
+ font-size: 1.5rem;
+ line-height: 1.5;
+ font-weight: 600;
+ label {
+ display: block;
+ margin-bottom: 1rem;
+ }
+ input,
+ textarea,
+ select {
+ width: 100%;
+ padding: 0.5rem;
+ font-size: 1rem;
+ border: 1px solid black;
+ &:focus {
+ outline: 0;
+ border-color: ${props => props.theme.red};
+ }
+ }
+ button,
+ input[type='submit'] {
+ width: auto;
+ background: red;
+ color: white;
+ border: 0;
+ font-size: 2rem;
+ font-weight: 600;
+ padding: 0.5rem 1.2rem;
+ }
+ fieldset {
+ border: 0;
+ padding: 0;
+
+ &[disabled] {
+ opacity: 0.5;
+ }
+ &::before {
+ height: 10px;
+ content: '';
+ display: block;
+ background-image: linear-gradient(to right, #ff3019 0%, #e2b04a 50%, #ff3019 100%);
+ }
+ &[aria-busy='true']::before {
+ background-size: 50% auto;
+ animation: ${loading} 0.5s linear infinite;
+ }
+ }
+`;
+
+Form.displayName = 'Form';
+
+export default Form;
diff --git a/finished-application/frontend/components/styles/ItemStyles.js b/finished-application/frontend/components/styles/ItemStyles.js
new file mode 100644
index 0000000..b117d5f
--- /dev/null
+++ b/finished-application/frontend/components/styles/ItemStyles.js
@@ -0,0 +1,39 @@
+import styled from 'styled-components';
+
+const Item = styled.div`
+ background: white;
+ border: 1px solid ${props => props.theme.offWhite};
+ box-shadow: ${props => props.theme.bs};
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ img {
+ width: 100%;
+ height: 400px;
+ object-fit: cover;
+ }
+ p {
+ font-size: 12px;
+ line-height: 2;
+ font-weight: 300;
+ flex-grow: 1;
+ padding: 0 3rem;
+ font-size: 1.5rem;
+ }
+ .buttonList {
+ display: grid;
+ width: 100%;
+ border-top: 1px solid ${props => props.theme.lightgrey};
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ grid-gap: 1px;
+ background: ${props => props.theme.lightgrey};
+ & > * {
+ background: white;
+ border: 0;
+ font-size: 1rem;
+ padding: 1rem;
+ }
+ }
+`;
+
+export default Item;
diff --git a/finished-application/frontend/components/styles/NavStyles.js b/finished-application/frontend/components/styles/NavStyles.js
new file mode 100644
index 0000000..41523df
--- /dev/null
+++ b/finished-application/frontend/components/styles/NavStyles.js
@@ -0,0 +1,64 @@
+import styled from 'styled-components';
+
+const NavStyles = styled.ul`
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-self: end;
+ font-size: 2rem;
+ a,
+ button {
+ padding: 1rem 3rem;
+ display: flex;
+ align-items: center;
+ position: relative;
+ text-transform: uppercase;
+ font-weight: 900;
+ font-size: 1em;
+ background: none;
+ border: 0;
+ cursor: pointer;
+ @media (max-width: 700px) {
+ font-size: 10px;
+ padding: 0 10px;
+ }
+ &:before {
+ content: '';
+ width: 2px;
+ background: ${props => props.theme.lightgrey};
+ height: 100%;
+ left: 0;
+ position: absolute;
+ transform: skew(-20deg);
+ top: 0;
+ bottom: 0;
+ }
+ &:after {
+ height: 2px;
+ background: red;
+ content: '';
+ width: 0;
+ position: absolute;
+ transform: translateX(-50%);
+ transition: width 0.4s;
+ transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
+ left: 50%;
+ margin-top: 2rem;
+ }
+ &:hover,
+ &:focus {
+ outline: none;
+ &:after {
+ width: calc(100% - 60px);
+ }
+ }
+ }
+ @media (max-width: 1300px) {
+ border-top: 1px solid ${props => props.theme.lightgrey};
+ width: 100%;
+ justify-content: center;
+ font-size: 1.5rem;
+ }
+`;
+
+export default NavStyles;
diff --git a/finished-application/frontend/components/styles/OrderItemStyles.js b/finished-application/frontend/components/styles/OrderItemStyles.js
new file mode 100644
index 0000000..292c75d
--- /dev/null
+++ b/finished-application/frontend/components/styles/OrderItemStyles.js
@@ -0,0 +1,44 @@
+import styled from 'styled-components';
+
+const OrderItemStyles = styled.li`
+ box-shadow: ${props => props.theme.bs};
+ list-style: none;
+ padding: 2rem;
+ border: 1px solid ${props => props.theme.offWhite};
+ h2 {
+ border-bottom: 2px solid red;
+ margin-top: 0;
+ margin-bottom: 2rem;
+ padding-bottom: 2rem;
+ }
+
+ .images {
+ display: grid;
+ grid-gap: 10px;
+ grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
+ margin-top: 1rem;
+ img {
+ height: 200px;
+ object-fit: cover;
+ width: 100%;
+ }
+ }
+ .order-meta {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
+ display: grid;
+ grid-gap: 1rem;
+ text-align: center;
+ & > * {
+ margin: 0;
+ background: rgba(0, 0, 0, 0.03);
+ padding: 1rem 0;
+ }
+ strong {
+ display: block;
+ margin-bottom: 1rem;
+ }
+ }
+`;
+
+export default OrderItemStyles;
diff --git a/finished-application/frontend/components/styles/OrderStyles.js b/finished-application/frontend/components/styles/OrderStyles.js
new file mode 100644
index 0000000..4a2d804
--- /dev/null
+++ b/finished-application/frontend/components/styles/OrderStyles.js
@@ -0,0 +1,38 @@
+import styled from 'styled-components';
+
+const OrderStyles = styled.div`
+ max-width: 1000px;
+ margin: 0 auto;
+ border: 1px solid ${props => props.theme.offWhite};
+ box-shadow: ${props => props.theme.bs};
+ padding: 2rem;
+ border-top: 10px solid red;
+ & > p {
+ display: grid;
+ grid-template-columns: 1fr 5fr;
+ margin: 0;
+ border-bottom: 1px solid ${props => props.theme.offWhite};
+ span {
+ padding: 1rem;
+ &:first-child {
+ font-weight: 900;
+ text-align: right;
+ }
+ }
+ }
+ .order-item {
+ border-bottom: 1px solid ${props => props.theme.offWhite};
+ display: grid;
+ grid-template-columns: 300px 1fr;
+ align-items: center;
+ grid-gap: 2rem;
+ margin: 2rem 0;
+ padding-bottom: 2rem;
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+`;
+export default OrderStyles;
diff --git a/finished-application/frontend/components/styles/PriceTag.js b/finished-application/frontend/components/styles/PriceTag.js
new file mode 100644
index 0000000..9116681
--- /dev/null
+++ b/finished-application/frontend/components/styles/PriceTag.js
@@ -0,0 +1,17 @@
+import styled from 'styled-components';
+
+const PriceTag = styled.span`
+ background: ${props => props.theme.red};
+ transform: rotate(3deg);
+ color: white;
+ font-weight: 600;
+ padding: 5px;
+ line-height: 1;
+ font-size: 3rem;
+ display: inline-block;
+ position: absolute;
+ top: -3px;
+ right: -3px;
+`;
+
+export default PriceTag;
diff --git a/finished-application/frontend/components/styles/SickButton.js b/finished-application/frontend/components/styles/SickButton.js
new file mode 100644
index 0000000..5b5352e
--- /dev/null
+++ b/finished-application/frontend/components/styles/SickButton.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+const SickButton = styled.button`
+ background: red;
+ color: white;
+ font-weight: 500;
+ border: 0;
+ border-radius: 0;
+ text-transform: uppercase;
+ font-size: 2rem;
+ padding: 0.8rem 1.5rem;
+ transform: skew(-2deg);
+ display: inline-block;
+ transition: all 0.5s;
+ &[disabled] {
+ opacity: 0.5;
+ }
+`;
+
+export default SickButton;
diff --git a/finished-application/frontend/components/styles/Supreme.js b/finished-application/frontend/components/styles/Supreme.js
new file mode 100644
index 0000000..d946ccd
--- /dev/null
+++ b/finished-application/frontend/components/styles/Supreme.js
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+const Supreme = styled.h3`
+ background: ${props => props.theme.red};
+ color: white;
+ display: inline-block;
+ padding: 4px 5px;
+ transform: skew(-3deg);
+ margin: 0;
+ font-size: 4rem;
+`;
+
+export default Supreme;
diff --git a/finished-application/frontend/components/styles/Table.js b/finished-application/frontend/components/styles/Table.js
new file mode 100644
index 0000000..e9d0673
--- /dev/null
+++ b/finished-application/frontend/components/styles/Table.js
@@ -0,0 +1,31 @@
+import styled from 'styled-components';
+
+const Table = styled.table`
+ border-spacing: 0;
+ width: 100%;
+ border: 1px solid ${props => props.theme.offWhite};
+ thead {
+ font-size: 10px;
+ }
+ td,
+ th {
+ border-bottom: 1px solid ${props => props.theme.offWhite};
+ border-right: 1px solid ${props => props.theme.offWhite};
+ padding: 10px 5px;
+ position: relative;
+ &:last-child {
+ border-right: none;
+ width: 150px;
+ button {
+ width: 100%;
+ }
+ }
+ }
+ tr {
+ &:hover {
+ background: ${props => props.theme.offWhite};
+ }
+ }
+`;
+
+export default Table;
diff --git a/finished-application/frontend/components/styles/Title.js b/finished-application/frontend/components/styles/Title.js
new file mode 100644
index 0000000..e1901ec
--- /dev/null
+++ b/finished-application/frontend/components/styles/Title.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+const Title = styled.h3`
+ margin: 0 1rem;
+ text-align: center;
+ transform: skew(-5deg) rotate(-1deg);
+ margin-top: -3rem;
+ text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
+ a {
+ background: ${props => props.theme.red};
+ display: inline;
+ line-height: 1.3;
+ font-size: 4rem;
+ text-align: center;
+ color: white;
+ padding: 0 1rem;
+ }
+`;
+
+export default Title;