From 3b45dd7b593825721ec9ee9f6a49c732601b1ae0 Mon Sep 17 00:00:00 2001 From: Wes Bos Date: Thu, 21 Sep 2017 15:39:12 -0400 Subject: Cha Ching --- components/AddToCart.js | 84 +++++++++++++++++++++++++++----- components/Cart.js | 35 +++++++------ components/CartList.js | 54 +++++++++----------- components/ChaChing.js | 56 +++++++++++++++++++++ components/CreateItem.js | 113 +++++++++++++++++++++++++----------------- components/ErrorMessage.js | 32 ++++++++++++ components/Item.js | 92 +++++++++++++++++++++++------------ components/Items.js | 119 ++++++++++++--------------------------------- components/LoginAuth0.js | 58 +++++++++++----------- components/Meta.js | 7 ++- components/Nav.js | 54 ++++++++++++++++---- components/Page.js | 10 ++-- components/Pagination.js | 82 +++++++++++++++---------------- components/Search.js | 103 ++++++++++++++++++--------------------- components/SingleItem.js | 36 ++++++++++++++ 15 files changed, 566 insertions(+), 369 deletions(-) create mode 100644 components/ChaChing.js create mode 100644 components/ErrorMessage.js create mode 100644 components/SingleItem.js (limited to 'components') diff --git a/components/AddToCart.js b/components/AddToCart.js index 0c40efe..9bcdb7f 100644 --- a/components/AddToCart.js +++ b/components/AddToCart.js @@ -1,6 +1,37 @@ import { Component } from 'react'; -import { ADD_TO_CART_MUTATION, CURRENT_USER_QUERY } from '../queries'; +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'; + +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 { componentDidMount() { @@ -12,28 +43,55 @@ class AddToCart extends Component { variables: { userId: this.props.currentUserQuery.user.id, itemId: this.props.id, - } + }, }); this.props.currentUserQuery.refetch(); - console.log(res) - } + 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; + if (!user) return

Loading...

; 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(); + console.log({ x, y }); + return (
- - { - cartIds.includes(this.props.id) - ? - : - } + {isInCart ? ( + + ) : ( + + )} + + {status => } +
- ) + ); } } -const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' }); const createOrderEnhancer = graphql(ADD_TO_CART_MUTATION, { name: 'addToCart' }); -export default compose(userEnhancer, createOrderEnhancer)(AddToCart); +const singleItemEnhancer = graphql(SINGLE_ITEM_QUERY, { + name: 'singleItemQuery', + options: ({ id }) => ({ + variables: { + id, + }, + }), +}); +export default compose(userEnhancer, createOrderEnhancer, removeFromCartEnhancer, singleItemEnhancer)(AddToCart); diff --git a/components/Cart.js b/components/Cart.js index 57a00fc..88b072c 100644 --- a/components/Cart.js +++ b/components/Cart.js @@ -1,12 +1,17 @@ -import { Component } from "react"; -import { graphql, compose } from "react-apollo"; -import { CURRENT_USER_QUERY } from "../queries"; -import styled from "styled-components"; -import formatMoney from "../lib/formatMoney.js"; +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` +const CartStyles = styled.div` background: white; + border-radius: 10px; padding: 20px; + overflow: hidden; + display: flex; + align-items: center; `; class Cart extends Component { @@ -14,7 +19,6 @@ class Cart extends Component { // 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 - console.log("refetching!"); setTimeout(this.props.currentUserQuery.refetch, 1); } @@ -23,19 +27,20 @@ class Cart extends Component { const { loading, error } = this.props.currentUserQuery; const { user } = this.props.currentUserQuery; if (loading || error || !user) return

Cart Loading...

; - const { email = "" } = user; const total = user.cart.reduce((a, b) => a + b.price, 0); return ( -
-

- πŸ’° There are {user.cart.length} Items in your cart - totaling {formatMoney(total)} -

-
+ + There + {user.cart.length === 1 ? ' is ' : 'are '} + + {user.cart.length === 1 ? ' item ' : ' items '} + in your cart totaling + + ); } } -const userEnhancer = graphql(CURRENT_USER_QUERY, { name: "currentUserQuery" }); +const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' }); export default compose(userEnhancer)(Cart); diff --git a/components/CartList.js b/components/CartList.js index d0456aa..d485907 100644 --- a/components/CartList.js +++ b/components/CartList.js @@ -5,31 +5,30 @@ 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 { 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'; -import TakeMyMoney from './TakeMyMoney' - -import { CURRENT_USER_QUERY, REMOVE_FROM_CART_MUTATION } from '../queries'; +import TakeMyMoney from './TakeMyMoney'; +import { removeFromCartEnhancer } from '../enhancers'; +import { CURRENT_USER_QUERY } from '../queries'; class CartList extends Component { componentDidMount() { setTimeout(this.props.currentUserQuery.refetch, 1); } render() { - - if(this.props.loading) { - return

Loading...

+ if (this.props.loading) { + return

Loading...

; } - if(this.props.error) { - return

Error...

+ if (this.props.error) { + return

Error...

; } - if(!has(this.props, 'currentUserQuery.user.cart')) { - return

Don't have it yet!

+ if (!has(this.props, 'currentUserQuery.user.cart')) { + return

Don't have it yet!

; } const cart = this.props.currentUserQuery.user.cart; @@ -43,32 +42,27 @@ class CartList extends Component { {cart.map(item => (
  • {item.title} - +
  • ))} - + - ) + ); } } const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' }); -const removeItemEnhancer = graphql(REMOVE_FROM_CART_MUTATION, { name: 'removeFromCart', options: { - update: (proxy, payload) => { - const data = proxy.readQuery({ query: CURRENT_USER_QUERY }); - const cartItemId = payload.data.removeFromCartItems.cartItem.id; - data.user.cart = data.user.cart.filter(item => item.id !== cartItemId); - proxy.writeQuery({ query: CURRENT_USER_QUERY, data }); - }, - } }); -export default compose(userEnhancer, removeItemEnhancer)(CartList) +export default compose(userEnhancer, removeFromCartEnhancer)(CartList); diff --git a/components/ChaChing.js b/components/ChaChing.js new file mode 100644 index 0000000..5874c27 --- /dev/null +++ b/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 }) => ( + + + + {status => ( + + {amount} + + )} + + + +); + +export default ChaChing; diff --git a/components/CreateItem.js b/components/CreateItem.js index 3b86f2c..d9dc97b 100644 --- a/components/CreateItem.js +++ b/components/CreateItem.js @@ -1,99 +1,124 @@ -import React, { Component } from 'react' -import { graphql, gql } from 'react-apollo' +import React, { Component } from 'react'; +import { graphql, gql } from 'react-apollo'; import { ALL_ITEMS_QUERY, CREATE_LINK_MUTATION } from '../queries'; +import ErrorMessage from './ErrorMessage'; class CreateLink extends Component { - state = { description: '', title: '', image: '', price: 0, fullPrice: 0, - loading: false - } + loading: false, + error: { + message: 'shit!', + }, + }; componentWillReceiveProps(nextProps) { console.log(nextProps); } - uploadFile = async (e) => { + uploadFile = async e => { const files = e.currentTarget.files; - let data = new FormData(); + const data = new FormData(); data.append('data', files[0]); // use the file endpoint const res = await fetch('https://api.graph.cool/file/v1/cj5xz8szs28930145gct82bdj', { method: 'POST', - body: data + body: data, }); const file = await res.json(); console.log(file); this.setState({ image: file.id }); - } + }; + + _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.createLinkMutation({ + // 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 (
    - { this.state.loading ? 'LOADING...' : 'Ready!' } + {this.state.loading ? 'LOADING...' : 'Ready!'} + + this.setState({ error: {} })} />

    Image - +

    -

    Title - this.setState({ title: e.target.value })} type='text' placeholder='A description for the link'/> +

    + Title + this.setState({ title: e.target.value })} + type="text" + placeholder="A description for the link" + />

    - + + onChange={e => this.setState({ description: e.target.value })} + type="text" + placeholder="The desc for this item" + />
    - ) - } - - _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 }); - const res = await this.props.createLinkMutation({ - // pass in those variables from state - variables: { - description, - title, - price: parseInt(price), - fullPrice, - imageId: image - } - }); - 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_LINK_MUTATION, { name: 'createLinkMutation', options: { +export default graphql(CREATE_LINK_MUTATION, { + name: 'createLinkMutation', + 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 } }) => { - console.log({createItem, ALL_ITEMS_QUERY}); + console.log({ createItem, ALL_ITEMS_QUERY }); 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 data.allItems.unshift(createItem); // 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) + }, +})(CreateLink); diff --git a/components/ErrorMessage.js b/components/ErrorMessage.js new file mode 100644 index 0000000..12de51d --- /dev/null +++ b/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 ( + +

    {props.error.message}

    + +
    + ); +}; + +DisplayError.propTypes = { + error: PropTypes.object.isRequired, + onButtonClick: PropTypes.func.isRequired, +}; + +export default DisplayError; diff --git a/components/Item.js b/components/Item.js index 66ca66f..1b47fcf 100644 --- a/components/Item.js +++ b/components/Item.js @@ -1,33 +1,61 @@ -import React, { Component } from 'react' -import { graphql, gql, compose } from 'react-apollo' -import { SINGLE_ITEM_QUERY } from '../queries'; -import withData from '../lib/withData'; - -class Item extends Component { - render() { - if(!this.props.findItem.Item) return

    Not ready

    - console.log(this.props.findItem.Item); - - if(this.props.loading) return

    Loading...

    - if(this.props.error) return

    Error...

    - - const item = this.props.findItem.Item; - return ( -
    -

    Viewing {item.title}

    -
    - ) +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 ComponentWithMutations = compose( - graphql(SINGLE_ITEM_QUERY, { - name: 'findItem', - // This comes from Props - options: ({ id }) => ({ - variables: { id } - }) - }) -)(Item); - -export default ComponentWithMutations; +`; + +const ItemComponent = ({ item }) => ( + + {item.image ? {item.title} : null} +

    + + {item.title} + +

    + +

    {item.description}

    + {/* { + + + Edit ✏️ + + + } */} + + + + + + + +
    +); + +export default ItemComponent; diff --git a/components/Items.js b/components/Items.js index d212ead..053c9af 100644 --- a/components/Items.js +++ b/components/Items.js @@ -1,20 +1,12 @@ -import { Component } from 'react' -import { withApollo, graphql, compose } from 'react-apollo' -import UpdateItem from './UpdateItem'; -import Link from 'next/link'; +import { Component } from 'react'; +import { withApollo, graphql, compose } from 'react-apollo'; import styled from 'styled-components'; -import TakeMyMoney from './TakeMyMoney'; -import AddToCart from './AddToCart'; import Pagination from './Pagination'; -import formatMoney from '../lib/formatMoney'; -import makeImage from '../lib/image'; -import slugify from 'slugify'; +import Item from './Item'; import { ALL_ITEMS_QUERY, DELETE_ITEM_MUTATION } from '../queries'; -const Title = styled.h1` - font-size: 10px; -`; +const Title = styled.h1`font-size: 10px;`; const Items = styled.div` display: grid; @@ -22,50 +14,39 @@ const Items = styled.div` grid-gap: 20px; `; -const Item = styled.div` - background: #f3f3f3; - padding: 5px; - img { - width: 100%; - } -`; - 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) { + if (this.props.page !== nextProps.page) { this.prefetchNextItems(nextProps.page); } } - prefetchNextItems = (currentPage) => { + 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 - } - }) - } + skip: page * 3 - 3, + }, + }); + }; render() { - console.log("CLIENTTT!!", this.props.client); - // 1 if (this.props.allItemsQuery && this.props.allItemsQuery.loading) { - return
    Loading
    + return
    Loading
    ; } // 2 if (this.props.allItemsQuery && this.props.allItemsQuery.error) { - console.log(this.props.allItemsQuery.error) - return
    Error
    + console.log(this.props.allItemsQuery.error); + return
    Error
    ; } // 3 @@ -73,53 +54,12 @@ class ItemList extends Component { return (
    - + Items For Sale - - {itemsToRender.map((item,i) => ( - - { item.image ? : null } -

    - - {item.title} - -

    - -

    {item.description}

    - - Edit ✏️ - - - - - - - - - -
    - ))} -
    - + {itemsToRender.map((item, i) => )}
    - ) + ); } - } // 1 @@ -127,30 +67,31 @@ class ItemList extends Component { // We export the graphQL HOC - this will fetch the data and inject it into the ItemList compeont via props // Create some Enhancers -const itemsEnahncer = graphql(ALL_ITEMS_QUERY, { name: 'allItemsQuery', options({ page }) { - return { - variables: { - skip: (page * 3) - 3 - }, - } -}}); +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 delted item + // 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 whereever we have used this data on the page + // 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(itemsEnahncer, deleteItemEnhancer)(ItemList)); +export default withApollo(compose(itemEnhancer, deleteItemEnhancer)(ItemList)); diff --git a/components/LoginAuth0.js b/components/LoginAuth0.js index 262d8bd..8691e82 100644 --- a/components/LoginAuth0.js +++ b/components/LoginAuth0.js @@ -1,70 +1,70 @@ -import { Component, PropTypes } from 'react' -import Auth0Lock from 'auth0-lock' -import { graphql, compose } from 'react-apollo' +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`; - console.log(redirectUrl); + constructor(props) { + super(props); + if (typeof window === 'undefined') return; + const redirectUrl = `http://localhost:6969/signup`; this._lock = new Auth0Lock('l851ev2q8X48wf56eGLjIWFbMwwbvWPE', 'wesbos.auth0.com', { auth: { redirect: false, - } + }, }); } + componentDidMount() { + console.log('MOUNT'); + this._lock.on('authenticated', authResult => { + window.localStorage.setItem('auth0IdToken', authResult.idToken); + this.props.currentUserQuery.refetch(); + }); + // refech on page load because of the server render + this.props.currentUserQuery.refetch(); + } + logout = () => { window.localStorage.removeItem('auth0IdToken'); this.props.currentUserQuery.refetch(); - } + }; - createUser = () => { + createUser = async () => { const variables = { idToken: window.localStorage.getItem('auth0IdToken'), emailAddress: 'wesbos@gmail.com', - name: 'Hardcoded Wes' + name: 'Hardcoded Wes', }; // TODO - make a createUser function this.props .createUser({ variables }) .then(response => { // this.props.currentUserQuery.refetch(); - this.props.history.replace("/"); + this.props.history.replace('/'); }) .catch(e => { console.error(e); - this.props.history.replace("/"); + this.props.history.replace('/'); }); }; - componentDidMount() { - console.log('MOUNT'); - this._lock.on('authenticated', (authResult) => { - window.localStorage.setItem('auth0IdToken', authResult.idToken) - this.props.currentUserQuery.refetch(); - }) - } - _showLogin = () => { - this._lock.show() - } + this._lock.show(); + }; render() { const { user } = this.props.currentUserQuery; - if ( user ) { - return + if (user) { + return ; } return (
    - ) + ); } } const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' }); -export default compose(userEnhancer)(LoginAuth0) +export default compose(userEnhancer)(LoginAuth0); diff --git a/components/Meta.js b/components/Meta.js index 5ecea01..2655a21 100644 --- a/components/Meta.js +++ b/components/Meta.js @@ -1,17 +1,16 @@ import React from 'react'; - import Head from 'next/head'; -import Nav from './Nav'; -export default () => ( +const Meta = () => (
    -
    ); + +export default Meta; diff --git a/components/Nav.js b/components/Nav.js index ad0b05a..d1f341d 100644 --- a/components/Nav.js +++ b/components/Nav.js @@ -1,10 +1,46 @@ -import Link from 'next/link' +import Link from 'next/link'; +import styled from 'styled-components'; -export default () => ( - -) +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 = () => ( + + + Home + + + Sign Up + + + Add an Item + + + Orders + + + My Cart + + +); + +export default Nav; diff --git a/components/Page.js b/components/Page.js index c05eeb7..50a6135 100644 --- a/components/Page.js +++ b/components/Page.js @@ -1,5 +1,5 @@ -import Header from './Header' -import Meta from './Meta' +import Header from './Header'; +import Meta from './Meta'; import Nav from './Nav'; import styled from 'styled-components'; @@ -13,10 +13,10 @@ const StyledPage = styled.div` const Page = ({ children }) => ( - +