summaryrefslogtreecommitdiffstats
path: root/frontend/components
diff options
context:
space:
mode:
authorWes Bos <wesbos@gmail.com>2018-01-30 12:41:01 -0500
committerWes Bos <wesbos@gmail.com>2018-01-30 12:41:01 -0500
commitcd0b07b3adb36fb5ec098f9e511ab45d774fee7b (patch)
treea203e5010219ccea1acc6bbd2c0a4bcfa0a78615 /frontend/components
parent0a1648bf47490b312169c531aeb51ef4725e8d2e (diff)
Move to Prisma Backend
Diffstat (limited to 'frontend/components')
-rw-r--r--frontend/components/AddToCart.js100
-rw-r--r--frontend/components/Cart.js46
-rw-r--r--frontend/components/CartList.js114
-rw-r--r--frontend/components/ChaChing.js56
-rw-r--r--frontend/components/Count.js37
-rw-r--r--frontend/components/CreateItem.js128
-rw-r--r--frontend/components/ErrorMessage.js32
-rw-r--r--frontend/components/Header.js42
-rw-r--r--frontend/components/Item.js61
-rw-r--r--frontend/components/Items.js97
-rw-r--r--frontend/components/LoginAuth0.js76
-rw-r--r--frontend/components/Meta.js24
-rw-r--r--frontend/components/Nav.js46
-rw-r--r--frontend/components/OrderList.js52
-rw-r--r--frontend/components/Page.js24
-rw-r--r--frontend/components/Pagination.js43
-rw-r--r--frontend/components/Search.js68
-rw-r--r--frontend/components/Signup.js94
-rw-r--r--frontend/components/SingleItem.js36
-rw-r--r--frontend/components/TakeMyMoney.js43
-rw-r--r--frontend/components/UpdateItem.js75
21 files changed, 1294 insertions, 0 deletions
diff --git a/frontend/components/AddToCart.js b/frontend/components/AddToCart.js
new file mode 100644
index 0000000..e46a1f5
--- /dev/null
+++ b/frontend/components/AddToCart.js
@@ -0,0 +1,100 @@
+import { Component } from 'react';
+import { ADD_TO_CART_MUTATION, CURRENT_USER_QUERY, SINGLE_ITEM_QUERY } from '../queries';
+import { removeFromCartEnhancer, userEnhancer } from '../enhancers';
+import { graphql, compose } from 'react-apollo';
+import Transition from 'react-transition-group/Transition';
+import makeImage from '../lib/image';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+
+const JumpImg = styled.img`
+ border: 0 solid black;
+ max-width: 100px;
+ transition: all 0.5s;
+ position: fixed;
+ left: ${props => props.x}px;
+ top: -100%;
+ &.jump-entered {
+ border-color: green;
+ transform-origin: 0 0;
+ transform: scale(0);
+ top: ${props => props.y}px;
+ left: ${props => props.x}px;
+ }
+ &.jump-entering {
+ border-color: yellow;
+ top: ${props => props.y}px;
+ left: ${props => props.x}px;
+ }
+ &.jump-exited {
+ border-color: red;
+ }
+ &.jump-exiting {
+ border-color: yellow;
+ }
+`;
+
+class AddToCart extends Component {
+ static propTypes = {
+ currentUserQuery: PropTypes.object,
+ };
+
+ componentDidMount() {
+ this.props.currentUserQuery.refetch();
+ }
+
+ addToCart = async () => {
+ const res = await this.props.addToCart({
+ variables: {
+ userId: this.props.currentUserQuery.user.id,
+ itemId: this.props.id,
+ },
+ });
+ this.props.currentUserQuery.refetch();
+ console.log(res);
+ };
+
+ removeFromCart = async () => {
+ const res = await this.props.removeFromCart({
+ variables: {
+ userId: this.props.currentUserQuery.user.id,
+ itemId: this.props.id,
+ },
+ });
+ this.props.currentUserQuery.refetch();
+ console.log(res);
+ };
+
+ render() {
+ const user = this.props.currentUserQuery.user;
+ // TODO WTF
+ if (!user || this.props.singleItemQuery.loading || !this.props.singleItemQuery.Item) return <p>Loading...</p>;
+ const cartIds = user.cart.map(item => item.id);
+ const image = this.props.singleItemQuery.Item.image || {};
+ const isInCart = cartIds.includes(this.props.id);
+ const { x, y } = document.querySelector('.cart').getBoundingClientRect();
+ return (
+ <div>
+ {isInCart ? (
+ <button onClick={this.removeFromCart}>❌ Remove From Cart</button>
+ ) : (
+ <button onClick={this.addToCart}>Add To Cart πŸ‘œ</button>
+ )}
+ <Transition in={isInCart} timeout={1000}>
+ {status => <JumpImg x={x} y={y} src={makeImage(image)} className={`jump-${status}`} />}
+ </Transition>
+ </div>
+ );
+ }
+}
+
+const createOrderEnhancer = graphql(ADD_TO_CART_MUTATION, { name: 'addToCart' });
+const singleItemEnhancer = graphql(SINGLE_ITEM_QUERY, {
+ name: 'singleItemQuery',
+ options: ({ id }) => ({
+ variables: {
+ id,
+ },
+ }),
+});
+export default compose(userEnhancer, createOrderEnhancer, removeFromCartEnhancer, singleItemEnhancer)(AddToCart);
diff --git a/frontend/components/Cart.js b/frontend/components/Cart.js
new file mode 100644
index 0000000..88b072c
--- /dev/null
+++ b/frontend/components/Cart.js
@@ -0,0 +1,46 @@
+import { Component } from 'react';
+import { graphql, compose } from 'react-apollo';
+import styled from 'styled-components';
+import { CURRENT_USER_QUERY } from '../queries';
+import formatMoney from '../lib/formatMoney.js';
+import ChaChing from './ChaChing';
+
+const CartStyles = styled.div`
+ background: white;
+ border-radius: 10px;
+ padding: 20px;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+`;
+
+class Cart extends Component {
+ componentDidMount() {
+ // This fetches the new data, but doesn't populate the user via props
+ // this.props.currentUserQuery.refetch();
+ // This fetches the new data, and populates the user via props
+ setTimeout(this.props.currentUserQuery.refetch, 1);
+ }
+
+ render() {
+ // Check for loading state..
+ const { loading, error } = this.props.currentUserQuery;
+ const { user } = this.props.currentUserQuery;
+ if (loading || error || !user) return <p>Cart Loading...</p>;
+ const total = user.cart.reduce((a, b) => a + b.price, 0);
+
+ return (
+ <CartStyles className="cart">
+ There
+ {user.cart.length === 1 ? ' is ' : 'are '}
+ <ChaChing amount={user.cart.length} />
+ {user.cart.length === 1 ? ' item ' : ' items '}
+ in your cart totaling
+ <ChaChing amount={formatMoney(total)} />
+ </CartStyles>
+ );
+ }
+}
+
+const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' });
+export default compose(userEnhancer)(Cart);
diff --git a/frontend/components/CartList.js b/frontend/components/CartList.js
new file mode 100644
index 0000000..b54aa39
--- /dev/null
+++ b/frontend/components/CartList.js
@@ -0,0 +1,114 @@
+import { Component } from 'react';
+import withData from '../lib/withData';
+import Items from '../components/Items';
+import Signup from '../components/Signup';
+import LoginAuth0 from '../components/LoginAuth0';
+import Page from '../components/Page';
+import { USER_ORDERS_QUERY, GET_CART_STATE } from '../queries';
+import { graphql, compose } from 'react-apollo';
+import has from 'lodash.has';
+import get from 'lodash.get';
+import styled from 'styled-components';
+import formatMoney from '../lib/formatMoney';
+import makeImage from '../lib/image';
+import TakeMyMoney from './TakeMyMoney';
+import { removeFromCartEnhancer } from '../enhancers';
+import { CURRENT_USER_QUERY } from '../queries';
+import PropTypes from 'prop-types';
+
+const CartStyles = styled.div`
+ padding: 20px;
+ position: relative;
+ background: white;
+ position: fixed;
+ height: 100%;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ transform: translateX(100%);
+ transition: all 0.3s;
+ box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2);
+ z-index: 5;
+ ${props => props.open && `transform: translateX(0);`};
+ .toggleOpen {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform: translateX(-100%);
+ }
+`;
+
+class CartList extends Component {
+ static contextTypes = {
+ client: PropTypes.object,
+ };
+
+ componentDidMount() {
+ setTimeout(this.props.currentUserQuery.refetch, 1);
+ // Set the cart state to be closed
+ this.context.client.writeQuery({
+ query: GET_CART_STATE,
+ data: { ui: { isCartOpen: true, __typename: 'Network' } },
+ });
+ }
+
+ toggleOpen = () => {
+ this.props.uiQuery.updateQuery(() => ({ ui: { isCartOpen: false, __typename: 'Network' } }));
+ };
+
+ render() {
+ if (this.props.loading) {
+ return <p>Loading....</p>;
+ }
+
+ if (this.props.error) {
+ return <p>Error....</p>;
+ }
+
+ // if (!has(this.props, 'currentUserQuery.user.cart')) {
+ // return <p>Don't have it yet!</p>;
+ // }
+ // OMG Clean this up
+ let { user = {} } = this.props.currentUserQuery || {};
+ if (!user) user = {};
+ const cart = user.cart || [];
+ const userId = user.id;
+ // const userId = this.props.currentUserQuery.user.id;
+
+ const total = cart.reduce((a, b) => a + b.price, 0);
+ return (
+ <CartStyles open>
+ <button className="toggleOpen" onClick={this.toggleOpen}>
+ Open Cart
+ </button>
+ <h1>{cart.length} Items</h1>
+
+ <ul>
+ {cart.map(item => (
+ <li key={item.id}>
+ {item.title}
+ <button
+ onClick={() =>
+ this.props.removeFromCart({
+ variables: {
+ userId,
+ itemId: item.id,
+ },
+ })}
+ >
+ &times; Delete
+ </button>
+ </li>
+ ))}
+ </ul>
+ <TakeMyMoney amount={total} name="Testing 123" description="Test test 123">
+ <button>Buy for {formatMoney(total)}</button>
+ </TakeMyMoney>
+ </CartStyles>
+ );
+ }
+}
+
+const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' });
+const uiEnhancer = graphql(GET_CART_STATE, { name: 'uiQuery' });
+export default compose(userEnhancer, removeFromCartEnhancer, uiEnhancer)(CartList);
diff --git a/frontend/components/ChaChing.js b/frontend/components/ChaChing.js
new file mode 100644
index 0000000..5874c27
--- /dev/null
+++ b/frontend/components/ChaChing.js
@@ -0,0 +1,56 @@
+import TransitionGroup from 'react-transition-group/TransitionGroup';
+import Transition from 'react-transition-group/Transition';
+import styled from 'styled-components';
+
+const CartCount = styled.div`
+ padding: 10px;
+ transition: all 0.5s;
+ display: inline-block;
+ background: white;
+ background: linear-gradient(to bottom, #eeeeee 0%, #eeeeee 100%);
+ transform: translateY(100%);
+ z-index: 1;
+ position: relative;
+ &.cart-entering {
+ background: green;
+ transform: translateY(100%);
+ }
+ &.cart-entered {
+ transform: translateY(0);
+ }
+ &.cart-exiting {
+ position: absolute;
+ z-index: 0;
+ left: 0;
+ transform: translateY(-100%);
+ }
+`;
+
+const ChaChingStyles = styled.div`
+ position: relative;
+ display: inline-block;
+ overflow: hidden;
+`;
+
+const ChaChing = ({ amount }) => (
+ <ChaChingStyles>
+ <TransitionGroup>
+ <Transition
+ in
+ timeout={{
+ enter: 0,
+ exit: 500,
+ }}
+ key={amount}
+ >
+ {status => (
+ <CartCount className={`cart-${status}`} key={`count-${amount}`}>
+ {amount}
+ </CartCount>
+ )}
+ </Transition>
+ </TransitionGroup>
+ </ChaChingStyles>
+);
+
+export default ChaChing;
diff --git a/frontend/components/Count.js b/frontend/components/Count.js
new file mode 100644
index 0000000..800a902
--- /dev/null
+++ b/frontend/components/Count.js
@@ -0,0 +1,37 @@
+import React, { Component } from 'react'
+import { graphql, gql } from 'react-apollo'
+
+import { ALL_ITEMS_QUERY } from '../queries';
+
+class Count extends Component {
+
+ render() {
+
+ // 1
+ if (this.props.allLinksQuery && this.props.allLinksQuery.loading) {
+ return <div>Loading</div>
+ }
+
+ // 2
+ if (this.props.allLinksQuery && this.props.allLinksQuery.error) {
+ return <div>Error</div>
+ }
+
+ // 3
+ const itemsToRender = this.props.allLinksQuery.allItems
+
+ return (
+ <div>
+ <h1>There are {itemsToRender.length} items for sale</h1>
+ </div>
+ )
+ }
+
+}
+
+// 1
+
+// We export the graphQL HOC - this will fetch the data and inject it into the Count compeont via props
+
+export { ALL_ITEMS_QUERY }
+export default graphql(ALL_ITEMS_QUERY, { name: 'allLinksQuery' }) (Count)
diff --git a/frontend/components/CreateItem.js b/frontend/components/CreateItem.js
new file mode 100644
index 0000000..27b62e5
--- /dev/null
+++ b/frontend/components/CreateItem.js
@@ -0,0 +1,128 @@
+import React, { Component } from 'react';
+import { graphql, gql } from 'react-apollo';
+import { ALL_ITEMS_QUERY, CREATE_ITEM_MUTATION } from '../queries';
+import ErrorMessage from './ErrorMessage';
+import makeImage from '../lib/image';
+import { fileEndpoint } from '../config';
+
+class CreateLink extends Component {
+ state = {
+ description: '',
+ title: '',
+ image: '',
+ price: 0,
+ fullPrice: 0,
+ loading: false,
+ error: {
+ message: '',
+ },
+ };
+
+ componentWillReceiveProps(nextProps) {
+ console.log(nextProps);
+ }
+
+ uploadFile = async e => {
+ this.setState({ loading: true });
+ const files = e.currentTarget.files;
+
+ const data = new FormData();
+ data.append('data', files[0]);
+
+ // use the file endpoint
+ const res = await fetch(fileEndpoint, {
+ method: 'POST',
+ body: data,
+ });
+ const file = await res.json();
+ this.setState({ image: file.id, loading: false });
+ };
+
+ _createLink = async e => {
+ e.preventDefault();
+ // pull the values from state
+ const { description, title, image, price, fullPrice } = this.state;
+ // create a mutation
+ // TODO: handle any errors
+ // turn loading on
+ this.setState({ loading: true });
+ try {
+ const res = await this.props.createItemMutation({
+ // pass in those variables from state
+ variables: {
+ description,
+ title,
+ price: parseInt(price),
+ fullPrice,
+ imageId: image,
+ },
+ });
+ } catch (error) {
+ this.setState({ error });
+ console.log(error);
+ }
+ this.setState({ loading: false });
+ };
+
+ render() {
+ return (
+ <div>
+ {this.state.loading ? 'LOADING...' : 'Ready!'}
+
+ <ErrorMessage error={this.state.error} onButtonClick={() => this.setState({ error: {} })} />
+ <form onSubmit={this._createLink}>
+ <p>
+ Image
+ <input onChange={this.uploadFile} type="file" accept=".png, .jpg, .jpeg" />
+ </p>
+ <p>
+ Title
+ <input
+ value={this.state.title}
+ onChange={e => this.setState({ title: e.target.value })}
+ type="text"
+ placeholder="Title"
+ />
+ </p>
+ <label>
+ Price<input
+ type="number"
+ min="0"
+ value={this.state.price}
+ onChange={e => this.setState({ price: e.target.value })}
+ />
+ </label>
+ <textarea
+ value={this.state.description}
+ onChange={e => this.setState({ description: e.target.value })}
+ type="text"
+ placeholder="The desc for this item"
+ />
+ <button disabled={!this.state.loading} type="submit">
+ Submit
+ </button>
+ </form>
+ </div>
+ );
+ }
+}
+// When we submit this mutation, we need to update our store - we have a few ways to do that:
+// One - we can go nucular and run refetchQueries() which will just go get everything - this is easy, but at the cost of efficiency.
+
+export default graphql(CREATE_ITEM_MUTATION, {
+ name: 'createItemMutation',
+ options: {
+ // Easy, but slow
+ // refetchQueries: ['AllLinksQuery']
+ // This is much Better / efficient
+ // Notice how the variable is called createItem - that is because createItem is the name of the query!
+ update: (proxy, { data: { createItem } }) => {
+ const data = proxy.readQuery({ query: ALL_ITEMS_QUERY });
+ // data is our store, allItems is our sub-"state", it's just an array. We can just add it to
+ console.log({ createItem });
+ data.allItems = [createItem, ...data.allItems.slice(1, 3)];
+ // and then "set state", so it will update on the page. This will update the cache for us!
+ proxy.writeQuery({ query: ALL_ITEMS_QUERY, data });
+ },
+ },
+})(CreateLink);
diff --git a/frontend/components/ErrorMessage.js b/frontend/components/ErrorMessage.js
new file mode 100644
index 0000000..12de51d
--- /dev/null
+++ b/frontend/components/ErrorMessage.js
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+import React from 'react';
+
+import PropTypes from 'prop-types';
+
+const StyledError = styled.div`
+ background: pink;
+ border: 2px solid red;
+ padding: 20px;
+ display: flex;
+ p {
+ margin: 0;
+ flex: 1 0 auto;
+ }
+`;
+
+const DisplayError = props => {
+ if (!props.error || !props.error.message) return null;
+ return (
+ <StyledError>
+ <p>{props.error.message}</p>
+ <button onClick={props.onButtonClick}>&times;</button>
+ </StyledError>
+ );
+};
+
+DisplayError.propTypes = {
+ error: PropTypes.object.isRequired,
+ onButtonClick: PropTypes.func.isRequired,
+};
+
+export default DisplayError;
diff --git a/frontend/components/Header.js b/frontend/components/Header.js
new file mode 100644
index 0000000..cc4cca7
--- /dev/null
+++ b/frontend/components/Header.js
@@ -0,0 +1,42 @@
+import { Component } from 'react';
+import { graphql, compose } from 'react-apollo';
+import { CURRENT_USER_QUERY } from '../queries';
+import Cart from './Cart';
+import Login from './LoginAuth0';
+import Search from './Search';
+import NProgress from 'nprogress';
+import Router from 'next/router';
+
+Router.onRouteChangeStart = url => {
+ console.log(`Loading: ${url}`);
+ NProgress.start();
+};
+Router.onRouteChangeComplete = () => NProgress.done();
+Router.onRouteChangeError = () => NProgress.done();
+
+class Header extends Component {
+ componentDidMount() {
+ // This fetches the new data, but doesn't populate the user via props
+ // this.props.currentUserQuery.refetch();
+ // This fetches the new data, and populates the user via props
+ setTimeout(this.props.currentUserQuery.refetch, 1);
+ }
+
+ render() {
+ const user = this.props.currentUserQuery.user || {};
+ const { email = '' } = user;
+ return (
+ <div>
+ <p>
+ Signed in as <strong>{email}</strong>
+ </p>
+ <Login />
+ <Cart />
+ <Search />
+ </div>
+ );
+ }
+}
+
+const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' });
+export default compose(userEnhancer)(Header);
diff --git a/frontend/components/Item.js b/frontend/components/Item.js
new file mode 100644
index 0000000..1b47fcf
--- /dev/null
+++ b/frontend/components/Item.js
@@ -0,0 +1,61 @@
+import styled from 'styled-components';
+import slugify from 'slugify';
+import { Link } from '../routes';
+import AddToCart from './AddToCart';
+import makeImage from '../lib/image';
+import TakeMyMoney from './TakeMyMoney';
+import formatMoney from '../lib/formatMoney';
+
+const Item = styled.div`
+ background: #f3f3f3;
+ padding: 5px;
+ img {
+ width: 100%;
+ }
+`;
+
+const ItemComponent = ({ item }) => (
+ <Item key={item.id}>
+ {item.image ? <img key={item.image.secret} src={makeImage(item.image)} alt={item.title} /> : null}
+ <h3>
+ <Link
+ route="item"
+ params={{
+ slug: slugify(item.title),
+ itemId: item.id,
+ }}
+ >
+ <a>{item.title}</a>
+ </Link>
+ </h3>
+
+ <p>{item.description}</p>
+ {/* {
+
+ <Link
+ href={{
+ pathname: '/admin/update',
+ query: { id: item.id },
+ }}
+ >
+ <a>Edit ✏️</a>
+ </Link>
+
+ } */}
+
+ <TakeMyMoney
+ id={item.id}
+ amount={item.price}
+ name={item.title} // the pop-in header title
+ description={item.description} // the pop-in header subtitle
+ image={makeImage(item.image)}
+ >
+ <button>Buy for {formatMoney(item.price)}</button>
+ </TakeMyMoney>
+
+ <AddToCart id={item.id} />
+ <button onClick={() => this.props.removeItemMutation({ variables: { id: item.id } })}>&times; Delete item</button>
+ </Item>
+);
+
+export default ItemComponent;
diff --git a/frontend/components/Items.js b/frontend/components/Items.js
new file mode 100644
index 0000000..053c9af
--- /dev/null
+++ b/frontend/components/Items.js
@@ -0,0 +1,97 @@
+import { Component } from 'react';
+import { withApollo, graphql, compose } from 'react-apollo';
+import styled from 'styled-components';
+import Pagination from './Pagination';
+import Item from './Item';
+
+import { ALL_ITEMS_QUERY, DELETE_ITEM_MUTATION } from '../queries';
+
+const Title = styled.h1`font-size: 10px;`;
+
+const Items = styled.div`
+ display: grid;
+ grid-template-columns: repeat(4, calc(33% - 20px));
+ grid-gap: 20px;
+`;
+
+class ItemList extends Component {
+ componentDidMount() {
+ this.prefetchNextItems(this.props.page);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ // update the next items if the page prop changed
+ if (this.props.page !== nextProps.page) {
+ this.prefetchNextItems(nextProps.page);
+ }
+ }
+
+ prefetchNextItems = currentPage => {
+ const page = currentPage + 1;
+ console.log(`Prefetching Next items! Page ${page}`);
+ this.props.client.query({
+ query: ALL_ITEMS_QUERY,
+ variables: {
+ skip: page * 3 - 3,
+ },
+ });
+ };
+
+ render() {
+ // 1
+ if (this.props.allItemsQuery && this.props.allItemsQuery.loading) {
+ return <div>Loading</div>;
+ }
+
+ // 2
+ if (this.props.allItemsQuery && this.props.allItemsQuery.error) {
+ console.log(this.props.allItemsQuery.error);
+ return <div>Error</div>;
+ }
+
+ // 3
+ const itemsToRender = this.props.allItemsQuery.allItems;
+
+ return (
+ <div>
+ <Pagination page={this.props.page} />
+ <Title>Items For Sale</Title>
+ <Items key={this.props.page}>{itemsToRender.map((item, i) => <Item key={item.id} item={item} />)}</Items>
+ </div>
+ );
+ }
+}
+
+// 1
+
+// We export the graphQL HOC - this will fetch the data and inject it into the ItemList compeont via props
+
+// Create some Enhancers
+const itemEnhancer = graphql(ALL_ITEMS_QUERY, {
+ name: 'allItemsQuery',
+ options({ page }) {
+ return {
+ variables: {
+ skip: page * 3 - 3,
+ },
+ };
+ },
+});
+
+const deleteItemEnhancer = graphql(DELETE_ITEM_MUTATION, {
+ name: 'removeItemMutation',
+ options: {
+ update: (proxy, { data: { deleteItem } }) => {
+ // grab the data from our cache
+ const data = proxy.readQuery({ query: ALL_ITEMS_QUERY });
+
+ // filter out the deleted item
+ data.allItems = data.allItems.filter(item => item.id !== deleteItem.id);
+
+ // and then "set state" (update cache), so it will update wherever we have used this data on the page
+ proxy.writeQuery({ query: ALL_ITEMS_QUERY, data });
+ },
+ },
+});
+
+export default withApollo(compose(itemEnhancer, deleteItemEnhancer)(ItemList));
diff --git a/frontend/components/LoginAuth0.js b/frontend/components/LoginAuth0.js
new file mode 100644
index 0000000..b55ae4a
--- /dev/null
+++ b/frontend/components/LoginAuth0.js
@@ -0,0 +1,76 @@
+import { Component, PropTypes } from 'react';
+import Auth0Lock from 'auth0-lock';
+import { graphql, compose } from 'react-apollo';
+import { CURRENT_USER_QUERY } from '../queries/index';
+
+class LoginAuth0 extends Component {
+ constructor(props) {
+ super(props);
+ if (typeof window === 'undefined') return;
+ const redirectUrl = `http://localhost:3000/signup`;
+ this._lock = new Auth0Lock('r6D64Waoq7roAnv8GT04dnn1tpq9PAqu', 'wesbos.auth0.com', {
+ auth: {
+ redirect: false,
+ },
+ });
+ }
+
+ componentDidMount() {
+ this._lock.on('authenticated', authResult => {
+ console.log(authResult);
+ window.localStorage.setItem('auth0IdToken', authResult.idToken);
+ console.log('Im baaaack!');
+ this.props.currentUserQuery.refetch();
+ });
+
+ this._lock.on('authorization_error', err => {
+ console.error(err);
+ });
+
+ // refech on page load because of the server render
+ this.props.currentUserQuery.refetch();
+ }
+
+ logout = () => {
+ window.localStorage.removeItem('auth0IdToken');
+ this.props.currentUserQuery.refetch();
+ };
+
+ createUser = async () => {
+ const variables = {
+ idToken: window.localStorage.getItem('auth0IdToken'),
+ emailAddress: 'wesbos@gmail.com',
+ name: 'Hardcoded Wes',
+ };
+ // TODO - make a createUser function
+ this.props
+ .createUser({ variables })
+ .then(response => {
+ // this.props.currentUserQuery.refetch();
+ this.props.history.replace('/');
+ })
+ .catch(e => {
+ console.error(e);
+ this.props.history.replace('/');
+ });
+ };
+
+ _showLogin = () => {
+ this._lock.show();
+ };
+
+ render() {
+ const { user } = this.props.currentUserQuery;
+ if (user) {
+ return <button onClick={this.logout}>Log out πŸ‘‹</button>;
+ }
+ return (
+ <div>
+ <button onClick={this._showLogin}>Log in with Auth0 </button>
+ </div>
+ );
+ }
+}
+
+const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' });
+export default compose(userEnhancer)(LoginAuth0);
diff --git a/frontend/components/Meta.js b/frontend/components/Meta.js
new file mode 100644
index 0000000..d7a2e17
--- /dev/null
+++ b/frontend/components/Meta.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import Head from 'next/head';
+import { injectGlobal } from 'styled-components';
+
+const Meta = () => (
+ <div>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ {injectGlobal`
+ html {
+ background: white;
+ }
+ `}
+ <meta charSet="utf-8" />
+ <link rel="shortcut icon" href="https://wesbos.com/wp-content/themes/wb2014/i/crown-yellow-small.png" />
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
+ <link rel="stylesheet" type="text/css" href="/static/nprogress.css" />
+
+ <title>Sick Fits</title>
+ </Head>
+ </div>
+);
+
+export default Meta;
diff --git a/frontend/components/Nav.js b/frontend/components/Nav.js
new file mode 100644
index 0000000..d1f341d
--- /dev/null
+++ b/frontend/components/Nav.js
@@ -0,0 +1,46 @@
+import Link from 'next/link';
+import styled from 'styled-components';
+
+const StyledUl = styled.ul`
+ margin: 0;
+ padding: 0;
+ display: flex;
+ li {
+ display: flex;
+ flex: 1;
+ }
+ a {
+ padding: 10px;
+ flex: 1;
+ text-decoration: none;
+ text-align: center;
+ background: rgba(0, 0, 0, 0.2);
+ color: white;
+ margin-right: 20px;
+ &:hover {
+ background: rgba(0, 0, 0, 0.3);
+ }
+ }
+`;
+
+const Nav = () => (
+ <StyledUl>
+ <Link prefetch href="/">
+ <a>Home</a>
+ </Link>
+ <Link prefetch href="/signup">
+ <a>Sign Up</a>
+ </Link>
+ <Link prefetch href="/add">
+ <a>Add an Item</a>
+ </Link>
+ <Link prefetch href="/orders">
+ <a>Orders</a>
+ </Link>
+ <Link prefetch href="/cart">
+ <a>My Cart</a>
+ </Link>
+ </StyledUl>
+);
+
+export default Nav;
diff --git a/frontend/components/OrderList.js b/frontend/components/OrderList.js
new file mode 100644
index 0000000..c33f0b9
--- /dev/null
+++ b/frontend/components/OrderList.js
@@ -0,0 +1,52 @@
+import { Component } from 'react';
+import withData from '../lib/withData';
+import Items from '../components/Items';
+import CreateItem from '../components/CreateItem';
+import Signup from '../components/Signup';
+import LoginAuth0 from '../components/LoginAuth0';
+import Page from '../components/Page';
+import { USER_ORDERS_QUERY } from '../queries';
+import { graphql, compose } from 'react-apollo'
+import has from 'lodash.has';
+import get from 'lodash.get';
+import formatMoney from '../lib/formatMoney';
+import makeImage from '../lib/image';
+
+class OrderList extends Component {
+ componentDidMount() {
+ console.log('Mounting!');
+ }
+ render() {
+
+ if(this.props.loading) {
+ return <p>Loading...</p>
+ }
+
+ if(this.props.error) {
+ return <p>Error...</p>
+ }
+
+ if(!has(this.props, 'userOrdersQuery.user.orders')) {
+ return <p>Don't have it yet!</p>
+ }
+
+ const orders = this.props.userOrdersQuery.user.orders;
+
+ console.log(orders);
+ return (
+ <div>
+ <h2>You have {orders.length} Orders</h2>
+ <ul>
+ {orders.map(order => <li key={order.id}>
+ <img src={makeImage(order.item.image)} alt=""/>
+ <h3>{order.item.title} ({formatMoney(order.amount)}) </h3>
+ <p>{order.item.description}</p>
+ </li>)}
+ </ul>
+ </div>
+ )
+ }
+}
+
+const ordersEnhancer = graphql(USER_ORDERS_QUERY, { name: 'userOrdersQuery' });
+export default compose(ordersEnhancer)(OrderList)
diff --git a/frontend/components/Page.js b/frontend/components/Page.js
new file mode 100644
index 0000000..f45d421
--- /dev/null
+++ b/frontend/components/Page.js
@@ -0,0 +1,24 @@
+import styled from 'styled-components';
+import Header from './Header';
+import Meta from './Meta';
+import Nav from './Nav';
+import CartList from './CartList';
+
+const StyledPage = styled.div`
+ font-family: sans-serif;
+ color: #303030;
+ background: #efc600;
+ padding: 100px;
+`;
+
+const Page = ({ children }) => (
+ <StyledPage className="main">
+ <Meta />
+ <Nav />
+ <Header />
+ <CartList />
+ {children}
+ </StyledPage>
+);
+
+export default Page;
diff --git a/frontend/components/Pagination.js b/frontend/components/Pagination.js
new file mode 100644
index 0000000..dedd231
--- /dev/null
+++ b/frontend/components/Pagination.js
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import { graphql, gql, compose } from 'react-apollo';
+import { ALL_ITEMS_QUERY } from '../queries';
+import withData from '../lib/withData';
+import { Link } from '../routes';
+
+const Pagination = props => {
+ const { loading, error } = props.allItemsQuery;
+
+ if (loading) return <p>Loading Item...</p>;
+
+ const meta = props.allItemsQuery._allItemsMeta;
+ const { page } = props;
+ const pages = Math.floor(meta.count / 3);
+ return (
+ <div>
+ <p>
+ Page <strong>{page} </strong>
+ of
+ <strong>{pages} </strong>
+ -
+ <strong>{meta.count} </strong>
+ total
+ </p>
+
+ {page > 1 ? (
+ <Link prefetch route="items" params={{ page: page - 1 }}>
+ <a>←Prev</a>
+ </Link>
+ ) : null}
+
+ {page < pages ? (
+ <Link prefetch route="items" params={{ page: page + 1 }}>
+ <a>Next β†’</a>
+ </Link>
+ ) : null}
+ </div>
+ );
+};
+
+const ComponentWithMutations = compose(graphql(ALL_ITEMS_QUERY, { name: 'allItemsQuery' }))(Pagination);
+
+export default ComponentWithMutations;
diff --git a/frontend/components/Search.js b/frontend/components/Search.js
new file mode 100644
index 0000000..0ce136e
--- /dev/null
+++ b/frontend/components/Search.js
@@ -0,0 +1,68 @@
+import Downshift from 'downshift';
+import { SEARCH_ITEMS_QUERY } from '../queries';
+import { graphql, compose } from 'react-apollo';
+import makeImage from '../lib/image';
+import { Router } from '../routes';
+import slugify from 'slugify';
+
+function routeToItem(item) {
+ Router.pushRoute('item', {
+ slug: slugify(item.title),
+ itemId: item.id,
+ });
+}
+
+function BasicAutocomplete(props) {
+ const { items, onChange } = props;
+ return (
+ <Downshift onChange={routeToItem} itemToString={item => item.title}>
+ {({ getInputProps, getItemProps, isOpen, inputValue, selectedItem, highlightedIndex }) => (
+ <div>
+ <input
+ {...getInputProps({
+ placeholder: 'Search For Item',
+ onChange: e => props.refetch({ searchTerm: e.target.value }),
+ style: { fontSize: '20px', padding: '20px', display: 'block', width: '100%' },
+ })}
+ />
+ {isOpen ? (
+ <div>
+ {items.map((item, index) => (
+ <div
+ {...getItemProps({ item })}
+ key={item.id}
+ style={{
+ backgroundColor: highlightedIndex === index ? '#e8e8e8' : 'white',
+ borderLeft: highlightedIndex === index ? '10px solid #ffc600' : '10px solid white',
+ padding: '10px',
+ display: 'flex',
+ alignItems: 'center',
+ }}
+ >
+ <img width="50" style={{ 'margin-right': '10px' }} src={makeImage(item.image)} alt={item.title} />
+ {item.title}
+ </div>
+ ))}
+ </div>
+ ) : null}
+ </div>
+ )}
+ </Downshift>
+ );
+}
+
+const Search = props => (
+ <BasicAutocomplete
+ items={props.searchItems.allItems}
+ onChange={selectedItem => console.log(selectedItem)}
+ refetch={props.searchItems.refetch}
+ />
+);
+
+const searchEnhancer = graphql(SEARCH_ITEMS_QUERY, {
+ name: 'searchItems',
+ options: {
+ variables: { searchTerm: 'camo' },
+ },
+});
+export default compose(searchEnhancer)(Search);
diff --git a/frontend/components/Signup.js b/frontend/components/Signup.js
new file mode 100644
index 0000000..57e2569
--- /dev/null
+++ b/frontend/components/Signup.js
@@ -0,0 +1,94 @@
+import React, { Component } from 'react';
+import { graphql, gql } from 'react-apollo';
+import { CREATE_USER_MUTATION } from '../queries';
+
+class Signup extends Component {
+ constructor() {
+ super();
+ const idToken = typeof window !== 'undefined' ? localStorage.getItem('auth0IdToken') || '' : '';
+ this.state = {
+ email: '',
+ name: '',
+ idToken,
+ error: undefined,
+ };
+ }
+
+ render() {
+ return (
+ <div>
+ {this.state.loading ? 'LOADING...' : 'Ready!'}
+
+ {this.state.error ? <p>{this.state.error.message}</p> : ''}
+
+ <form onSubmit={this._createUser}>
+ <p>
+ Email
+ <input
+ value={this.state.email}
+ onChange={e => this.setState({ email: e.target.value })}
+ type="text"
+ placeholder="email"
+ />
+ </p>
+
+ <p>
+ Name
+ <input
+ value={this.state.name}
+ onChange={e => this.setState({ name: e.target.value })}
+ type="text"
+ placeholder="name"
+ />
+ </p>
+
+ <p>
+ idToken
+ <input
+ disabled
+ value={this.state.idToken}
+ onChange={e => this.setState({ idToken: e.target.value })}
+ type="text"
+ placeholder="name"
+ />
+ </p>
+
+ <button type="submit">Submit</button>
+ </form>
+ </div>
+ );
+ }
+
+ _createUser = async e => {
+ e.preventDefault();
+ // pull the values from state
+ const { email, name, idToken } = this.state;
+ // create a mutation
+ // TODO: handle any errors
+ // turn loading on
+ this.setState({ loading: true });
+ console.log(name, email, idToken);
+ try {
+ const res = await this.props.createUserMutation({
+ // pass in those variables from state
+ variables: { name, email, idToken },
+ });
+ } catch (error) {
+ this.setState({ error });
+ console.dir(error);
+ }
+ this.setState({ loading: false });
+ };
+}
+// When we submit this mutation, we need to update our store - we have a few ways to do that:
+// One - we can go nucular and run refetchQueries() which will just go get everything - this is easy, but at the cost of efficiency.
+
+export default graphql(CREATE_USER_MUTATION, {
+ name: 'createUserMutation',
+ options: {
+ // Easy, but slow
+ // refetchQueries: ['AllLinksQuery']
+ // This is much Better / efficient
+ // Notice how the variable is called createItem - that is because createItem is the name of the query!
+ },
+})(Signup);
diff --git a/frontend/components/SingleItem.js b/frontend/components/SingleItem.js
new file mode 100644
index 0000000..297c33d
--- /dev/null
+++ b/frontend/components/SingleItem.js
@@ -0,0 +1,36 @@
+import { graphql, compose } from 'react-apollo';
+import { Motion, spring } from 'react-motion';
+import { SINGLE_ITEM_QUERY } from '../queries';
+import makeImage from '../lib/image';
+
+const SingleItem = props => {
+ if (!props.findItem.Item) return <p>Not ready</p>;
+ console.log(props.findItem.Item);
+
+ if (props.loading) return <p>Loading...</p>;
+ if (props.error) return <p>Error...</p>;
+
+ const item = props.findItem.Item;
+ return (
+ <div>
+ <img src={makeImage(item.image)} alt={item.title} />
+ <h2>Viewing {item.title}</h2>
+
+ <Motion defaultStyle={{ x: 0 }} style={{ x: spring(100) }}>
+ {value => <div>{value.x}</div>}
+ </Motion>
+ </div>
+ );
+};
+
+const ComponentWithMutations = compose(
+ graphql(SINGLE_ITEM_QUERY, {
+ name: 'findItem',
+ // This comes from Props
+ options: ({ id }) => ({
+ variables: { id },
+ }),
+ })
+)(SingleItem);
+
+export default ComponentWithMutations;
diff --git a/frontend/components/TakeMyMoney.js b/frontend/components/TakeMyMoney.js
new file mode 100644
index 0000000..d68cca1
--- /dev/null
+++ b/frontend/components/TakeMyMoney.js
@@ -0,0 +1,43 @@
+import { Component } from 'react';
+import StripeCheckout from 'react-stripe-checkout';
+import { CREATE_ORDER_MUTATION, CURRENT_USER_QUERY } from '../queries';
+import { graphql, compose } from 'react-apollo';
+
+
+class TakeMyMoney extends Component {
+ onToken = async (res) => {
+ const token = res.id;
+ const userId = this.props.currentUserQuery.user.id;
+ const itemId = this.props.id;
+ console.log(`Going to make a purchase with ${token}`);
+ console.log(`THe person that bought this was ${userId}`)
+ console.log(`The item id is ${itemId}`)
+ const charge = await this.props.createOrder({ variables: { token, userId, itemId }});
+ alert(`Back from the charge! ${charge.id}`);
+ console.log(charge);
+
+ }
+ render() {
+ const user = this.props.currentUserQuery.user || {};
+ const email = user.email || '';
+ return (
+ <div>
+ <StripeCheckout
+ amount={this.props.amount}
+ name={this.props.name}
+ description={this.props.description}
+ token={this.onToken}
+ stripeKey="pk_lclTtThFp8CnO3QtEZSd8HA9mFUps"
+ currency="USD"
+ email={email}
+ >
+ {this.props.children}
+ </StripeCheckout>
+ </div>
+ )
+ }
+}
+
+const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' });
+const createOrderEnhancer = graphql(CREATE_ORDER_MUTATION, { name: 'createOrder' });
+export default compose(userEnhancer, createOrderEnhancer)(TakeMyMoney);
diff --git a/frontend/components/UpdateItem.js b/frontend/components/UpdateItem.js
new file mode 100644
index 0000000..27be90f
--- /dev/null
+++ b/frontend/components/UpdateItem.js
@@ -0,0 +1,75 @@
+import React, { Component } from 'react';
+import { graphql, gql, compose } from 'react-apollo';
+import { SINGLE_ITEM_QUERY, UPDATE_LINK_MUTATION } from '../queries';
+
+class UpdateLink extends Component {
+ state = {
+ ...this.props.findItem.Item,
+ };
+
+ saveToState = e => {
+ let { name, value, type } = e.target;
+ if (type === 'number') {
+ value = parseInt(value);
+ }
+
+ this.setState({ [name]: value });
+ };
+
+ _createLink = async e => {
+ e.preventDefault();
+ // pull the values from state
+ const { description, title } = this.state;
+ const { id } = this.props;
+ // create a mutation
+ // TODO: handle any errors
+ // turn loading on
+ this.setState({ loading: true });
+ console.log(this.state);
+ const res = await this.props.updateItem({
+ // pass in those variables from state
+ variables: {
+ ...this.state,
+ },
+ });
+ this.setState({ loading: false });
+ };
+
+ render() {
+ return (
+ <div>
+ <h2>Edit {this.props.id}</h2>
+ {this.state.loading ? 'LOADING...' : 'Ready!'}
+ <form onSubmit={this._createLink}>
+ <label htmlFor="title">Title</label>
+ <input value={this.state.title} name="title" onChange={this.saveToState} type="text" />
+
+ <label htmlFor="description">Description</label>
+ <textarea value={this.state.description} name="description" onChange={this.saveToState} />
+
+ <label htmlFor="price">Price</label>
+ <input type="number" name="price" onChange={this.saveToState} value={this.state.price} />
+
+ <label htmlFor="fullPrice">Full Price</label>
+ <input type="number" name="fullPrice" onChange={this.saveToState} value={this.state.fullPrice} />
+
+ <button type="submit">Save...</button>
+ </form>
+ </div>
+ );
+ }
+}
+
+const ComponentWithMutations = compose(
+ // First, query for getting the link
+ graphql(SINGLE_ITEM_QUERY, {
+ name: 'findItem',
+ options: ({ id }) => ({
+ variables: { id },
+ }),
+ }),
+ // Second, the mutation for updating the link
+ graphql(UPDATE_LINK_MUTATION, { name: 'updateItem' })
+)(UpdateLink);
+
+export default ComponentWithMutations;