diff options
| author | Wes Bos <wesbos@gmail.com> | 2018-01-30 12:41:01 -0500 |
|---|---|---|
| committer | Wes Bos <wesbos@gmail.com> | 2018-01-30 12:41:01 -0500 |
| commit | cd0b07b3adb36fb5ec098f9e511ab45d774fee7b (patch) | |
| tree | a203e5010219ccea1acc6bbd2c0a4bcfa0a78615 /frontend/components | |
| parent | 0a1648bf47490b312169c531aeb51ef4725e8d2e (diff) | |
Move to Prisma Backend
Diffstat (limited to 'frontend/components')
| -rw-r--r-- | frontend/components/AddToCart.js | 100 | ||||
| -rw-r--r-- | frontend/components/Cart.js | 46 | ||||
| -rw-r--r-- | frontend/components/CartList.js | 114 | ||||
| -rw-r--r-- | frontend/components/ChaChing.js | 56 | ||||
| -rw-r--r-- | frontend/components/Count.js | 37 | ||||
| -rw-r--r-- | frontend/components/CreateItem.js | 128 | ||||
| -rw-r--r-- | frontend/components/ErrorMessage.js | 32 | ||||
| -rw-r--r-- | frontend/components/Header.js | 42 | ||||
| -rw-r--r-- | frontend/components/Item.js | 61 | ||||
| -rw-r--r-- | frontend/components/Items.js | 97 | ||||
| -rw-r--r-- | frontend/components/LoginAuth0.js | 76 | ||||
| -rw-r--r-- | frontend/components/Meta.js | 24 | ||||
| -rw-r--r-- | frontend/components/Nav.js | 46 | ||||
| -rw-r--r-- | frontend/components/OrderList.js | 52 | ||||
| -rw-r--r-- | frontend/components/Page.js | 24 | ||||
| -rw-r--r-- | frontend/components/Pagination.js | 43 | ||||
| -rw-r--r-- | frontend/components/Search.js | 68 | ||||
| -rw-r--r-- | frontend/components/Signup.js | 94 | ||||
| -rw-r--r-- | frontend/components/SingleItem.js | 36 | ||||
| -rw-r--r-- | frontend/components/TakeMyMoney.js | 43 | ||||
| -rw-r--r-- | frontend/components/UpdateItem.js | 75 |
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, + }, + })} + > + × 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}>×</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 } })}>× 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; |
