diff options
Diffstat (limited to 'frontend/components')
| -rw-r--r-- | frontend/components/AddToCart.js | 7 | ||||
| -rw-r--r-- | frontend/components/CreateItem.js | 5 | ||||
| -rw-r--r-- | frontend/components/ErrorMessage.js | 4 | ||||
| -rw-r--r-- | frontend/components/Item.js | 3 | ||||
| -rw-r--r-- | frontend/components/Order.js | 60 | ||||
| -rw-r--r-- | frontend/components/OrderList.js | 102 | ||||
| -rw-r--r-- | frontend/components/Page.js | 10 | ||||
| -rw-r--r-- | frontend/components/Permissions.js | 128 | ||||
| -rw-r--r-- | frontend/components/Signin.js | 84 | ||||
| -rw-r--r-- | frontend/components/SingleItem.js | 39 | ||||
| -rw-r--r-- | frontend/components/styles/SickButton.js | 15 |
11 files changed, 379 insertions, 78 deletions
diff --git a/frontend/components/AddToCart.js b/frontend/components/AddToCart.js index 4757296..1d20c0c 100644 --- a/frontend/components/AddToCart.js +++ b/frontend/components/AddToCart.js @@ -9,6 +9,7 @@ class AddToCart extends Component { }; update = (proxy, payload) => { + console.log('Updating!'); const newCartItem = payload.data.addToCart; const data = proxy.readQuery({ query: CURRENT_USER_QUERY }); const existingIndex = data.me.cart.findIndex(cartItem => cartItem.id === newCartItem.id); @@ -25,7 +26,11 @@ class AddToCart extends Component { const { id } = this.props; return ( <Mutation mutation={ADD_TO_CART_MUTATION} variables={{ id }} update={this.update}> - {addToCart => <button onClick={addToCart}>🛒 Add To Cart</button>} + {(addToCart, { loading }) => ( + <button disabled={loading} onClick={addToCart}> + 🛒 Add{loading && 'ing'} To Cart + </button> + )} </Mutation> ); } diff --git a/frontend/components/CreateItem.js b/frontend/components/CreateItem.js index d166c81..ece62b4 100644 --- a/frontend/components/CreateItem.js +++ b/frontend/components/CreateItem.js @@ -60,12 +60,13 @@ class CreateItem extends Component { <fieldset disabled={loading} aria-busy={loading}> <label> Image - <input onChange={this.uploadFile} type="file" accept=".png, .jpg, .jpeg" /> + <input required onChange={this.uploadFile} type="file" accept=".png, .jpg, .jpeg" /> {this.state.image ? <img src={this.state.image} width="100" alt={this.state.title} /> : null} </label> <label> Title <input + required value={this.state.title} onChange={this.handleChange} type="text" @@ -77,6 +78,7 @@ class CreateItem extends Component { <label> Price <input + required type="number" id="price" name="price" @@ -87,6 +89,7 @@ class CreateItem extends Component { </label> <textarea id="description" + required name="description" value={this.state.description} onChange={this.handleChange} diff --git a/frontend/components/ErrorMessage.js b/frontend/components/ErrorMessage.js index c473731..3731554 100644 --- a/frontend/components/ErrorMessage.js +++ b/frontend/components/ErrorMessage.js @@ -21,8 +21,8 @@ const StyledError = styled.div` const DisplayError = ({ error }) => { if (!error || !error.message) return null; if (error.networkError && error.networkError.result && error.networkError.result.errors.length) { - return error.networkError.result.errors.map(error => ( - <StyledError> + return error.networkError.result.errors.map((error, i) => ( + <StyledError key={i}> <p> <strong>Shoot!</strong> {error.message} diff --git a/frontend/components/Item.js b/frontend/components/Item.js index 9bff385..050ec17 100644 --- a/frontend/components/Item.js +++ b/frontend/components/Item.js @@ -10,11 +10,10 @@ import PropTypes from 'prop-types'; const Item = styled.div` background: white; border: 1px solid ${props => props.theme.offWhite}; - box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.09); + box-shadow: ${props => props.theme.bs}; position: relative; display: grid; align-content: start; - /* grid-template-rows: repeat(auto-fit, minmax(100px,)); */ grid-auto-rows: fit-content; img { width: 100%; diff --git a/frontend/components/Order.js b/frontend/components/Order.js index a715483..603aae5 100644 --- a/frontend/components/Order.js +++ b/frontend/components/Order.js @@ -3,8 +3,46 @@ import { Query } from 'react-apollo'; import { format } from 'date-fns'; import Head from 'next/head'; import PropTypes from 'prop-types'; +import styled from 'styled-components'; import { SINGLE_ORDER_QUERY } from '../queries'; import formatMoney from '../lib/formatMoney'; +import Dump from './Dump'; + +const OrderStyles = styled.div` + max-width: 1000px; + margin: 0 auto; + border: 1px solid ${props => props.theme.offWhite}; + box-shadow: ${props => props.theme.bs}; + padding: 2rem; + border-top: 10px solid red; + & > p { + display: grid; + grid-template-columns: 1fr 5fr; + margin: 0; + border-bottom: 1px solid ${props => props.theme.offWhite}; + span { + padding: 1rem; + &:first-child { + font-weight: 900; + text-align: right; + } + } + } + .order-item { + border-bottom: 1px solid ${props => props.theme.offWhite}; + display: grid; + grid-template-columns: 300px 1fr; + align-items: center; + grid-gap: 2rem; + margin: 2rem 0; + padding-bottom: 2rem; + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } +`; class Order extends Component { static propTypes = { @@ -18,10 +56,11 @@ class Order extends Component { if (loading) return <p>Loading...</p>; if (!order || error) return <p>No Order Found!</p>; return ( - <div> + <OrderStyles> <Head> <title>Sick Fits - Order {order.id}</title> </Head> + <Dump order={order} /> <p> <span>Order Id:</span> <span>{order.id}</span> @@ -38,17 +77,26 @@ class Order extends Component { <span>Order Total</span> <span>{formatMoney(order.total)}</span> </p> - <p>You Bought {order.items.length} items in this order</p> + <p> + <span>Item Count</span> + <span>{order.items.length}</span> + </p> <div className="items"> {order.items.map(item => ( <div className="order-item" key={item.id}> - <h2> - {item.quantity} OF {item.title} - </h2> + <img src={item.image} alt={item.title} /> + <div className="item-details"> + <h2> {item.title} </h2> + <p>Qty: {item.quantity}</p> + <p>Each: {formatMoney(item.price)}</p> + <p>Subtotal: {formatMoney(item.price * item.quantity)}</p> + <p>SubTotal: {item.description}</p> + <p>{item.description}</p> + </div> </div> ))} </div> - </div> + </OrderStyles> ); }} </Query> diff --git a/frontend/components/OrderList.js b/frontend/components/OrderList.js index ed7beae..5467f93 100644 --- a/frontend/components/OrderList.js +++ b/frontend/components/OrderList.js @@ -3,6 +3,54 @@ import { Query } from 'react-apollo'; import { format, formatDistance } from 'date-fns'; import Link from 'next/link'; import { USER_ORDERS_QUERY } from '../queries'; +import styled from 'styled-components'; +import formatMoney from '../lib/formatMoney'; + +const OrderUl = styled.ul` + display: grid; + grid-gap: 4rem; + grid-template-columns: repeat(auto-fit, minmax(40%, 1fr)); +`; +const OrderListItem = styled.li` + box-shadow: ${props => props.theme.bs}; + list-style: none; + padding: 2rem; + border: 1px solid ${props => props.theme.offWhite}; + h2 { + border-bottom: 2px solid red; + margin-top: 0; + margin-bottom: 2rem; + padding-bottom: 2rem; + } + + .images { + display: grid; + grid-gap: 10px; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + margin-top: 1rem; + img { + height: 200px; + object-fit: cover; + width: 100%; + } + } + .order-meta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(20px, 1fr)); + display: grid; + grid-gap: 1rem; + text-align: center; + & > * { + margin: 0; + background: rgba(0, 0, 0, 0.03); + padding: 1rem 0; + } + strong { + display: block; + margin-bottom: 1rem; + } + } +`; class OrderList extends React.Component { render() { @@ -15,28 +63,42 @@ class OrderList extends React.Component { return ( <div> <h2>You have {orders.length} Orders</h2> - <ul> + <OrderUl> {orders.map(order => ( - <li key={order.id}> - <ul className="order-items"> - <hr /> - <h2> - <Link - href={{ - pathname: '/order', - query: { id: order.id }, - }} - > - <a>{order.id}</a> - </Link> - <time dateTime={order.createdAt} title={format(order.createdAt, 'MMMM D, YYYY h:mm A')}> - {formatDistance(order.createdAt, new Date())} - </time> - </h2> - </ul> - </li> + <OrderListItem key={order.id}> + <Link + href={{ + pathname: '/order', + query: { id: order.id }, + }} + > + <a> + <div className="order-meta"> + <p> + <strong>{order.items.reduce((a, b) => a + b.quantity, 0)}</strong> + Items + </p> + <p> + <strong>{order.items.length}</strong> + Products + </p> + <p> + <strong>{formatDistance(order.createdAt, new Date())}</strong> + ago + </p> + <p> + <strong>{formatMoney(order.total)}</strong> + Total + </p> + </div> + <div className="images"> + {order.items.map(item => <img key={item.id} src={item.image} alt={item.title} />)} + </div> + </a> + </Link> + </OrderListItem> ))} - </ul> + </OrderUl> </div> ); }} diff --git a/frontend/components/Page.js b/frontend/components/Page.js index bfe8492..031536b 100644 --- a/frontend/components/Page.js +++ b/frontend/components/Page.js @@ -13,6 +13,7 @@ const theme = { lightgrey: '#E1E1E1', offWhite: '#EDEDED', maxWidth: '1300px', + bs: '0 12px 24px 0 rgba(0, 0, 0, 0.09)', }; injectGlobal` @@ -26,6 +27,7 @@ injectGlobal` background-color: #ffffff; margin: 0; font-size: 1.5rem; + line-height: 2; background: red; } *, *:before, *:after { @@ -37,6 +39,12 @@ injectGlobal` } `; +const Inner = styled.div` + max-width: 1000px; + margin: 0 auto; + padding: 2rem; +`; + const StyledPage = styled.div` color: ${props => props.theme.black}; background: white; @@ -48,7 +56,7 @@ const Page = ({ children }) => ( <StyledPage className="main"> <Meta /> <Header /> - {children} + <Inner>{children}</Inner> </StyledPage> </ThemeProvider> </UIProvider> diff --git a/frontend/components/Permissions.js b/frontend/components/Permissions.js new file mode 100644 index 0000000..0b3976c --- /dev/null +++ b/frontend/components/Permissions.js @@ -0,0 +1,128 @@ +import React from 'react'; +import { Query, Mutation } from 'react-apollo'; +import styled from 'styled-components'; +import { BarLoader } from 'react-spinners'; +import { perPage } from '../config'; +import { ALL_USERS_QUERY, UPDATE_PERMISSIONS_MUTATION } from '../queries/index'; +import Error from './ErrorMessage'; +import Form from './styles/Form'; +import SickButton from './styles/SickButton'; + +const PermissionsBox = styled.div` + border: 1px solid ${props => props.theme.offWhite}; + box-shadow: ${props => props.theme.bs}; + margin-bottom: 5rem; + padding: 2rem; + label { + cursor: pointer; + span { + transition: all 0.1s; + padding: 0 1rem; + display: block; + border: 1px solid ${props => props.theme.offWhite}; + border-left-width: 20px; + } + input { + display: none; + } + input:checked + span { + border-color: red; + } + margin-right: 1rem; + margin-bottom: 1rem; + } + .labels { + display: flex; + flex-wrap: wrap; + & > * { + flex: 0 1 auto; + } + } +`; + +class User extends React.Component { + state = { + permissions: this.props.user.permissions, + }; + handlePermissionsChange = e => { + const checkbox = e.target; + let updatedPermissions = [...this.state.permissions]; + if (checkbox.checked) { + // add it in + updatedPermissions.push(checkbox.value); + } else { + updatedPermissions = updatedPermissions.filter(permission => permission !== checkbox.value); + } + this.setState({ permissions: updatedPermissions }); + }; + render() { + const { user } = this.props; + return ( + <Mutation mutation={UPDATE_PERMISSIONS_MUTATION}> + {(updatePermissions, { loading, error }) => ( + <PermissionsBox key={user.id} className="user"> + <BarLoader className="barLoader" width="100%" height={5} color="red" loading={loading} /> + <Error error={error} /> + <h2> + {user.name} -- {user.email} + </h2> + <div className="labels"> + {[ + 'ADMIN', + 'USER', + 'ITEMCREATE', + 'ITEMUPDATE', + 'ITEMDELETE', + 'PERMISSIONUPDATE', + 'NUKE', + ].map(permission => ( + <label key={permission} htmlFor={`${user.id}-permission-${permission}`}> + <input + type="checkbox" + checked={this.state.permissions.includes(permission)} + name={`permission-${permission}`} + id={`${user.id}-permission-${permission}`} + onChange={this.handlePermissionsChange} + value={permission} + /> + <span>{permission}</span> + </label> + ))} + </div> + <SickButton + type="button" + onClick={async () => { + const res = await updatePermissions({ + variables: { + permissions: this.state.permissions, + userId: this.props.user.id, + }, + }); + console.log(res); + }} + > + Update Permissions + </SickButton> + </PermissionsBox> + )} + </Mutation> + ); + } +} + +const Permissions = () => ( + <Query query={ALL_USERS_QUERY}> + {({ data, error, loading }) => { + if (loading) return <div>Loading</div>; + if (error) return <Error error={error} />; + return ( + <div> + <h1>Manage User Permissions</h1> + {data.users.map(user => <User key={user.id} user={user} />)} + </div> + ); + }} + </Query> +); + +export default Permissions; diff --git a/frontend/components/Signin.js b/frontend/components/Signin.js index 9f9eb96..846fd0c 100644 --- a/frontend/components/Signin.js +++ b/frontend/components/Signin.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Mutation } from 'react-apollo'; +import { Mutation, Query } from 'react-apollo'; import { SIGNIN_MUTATION, CURRENT_USER_QUERY } from '../queries'; import Error from './ErrorMessage'; import Form from './styles/Form'; @@ -10,15 +10,19 @@ class Signin extends Component { password: 'abc123', }; - loginUser = async (e, signin) => { + loginUser = async (e, signin, refetchUser) => { e.preventDefault(); const res = await signin(); localStorage.setItem('token', res.data.signin.token); + await refetchUser(); // TODO refetch current user query }; - update = (proxy, result) => { - console.log(proxy, result); + update = (proxy, payload) => { + console.log(proxy); + const data = proxy.readQuery({ query: CURRENT_USER_QUERY }); + data.me = payload.data.signin.user; + proxy.writeQuery({ query: CURRENT_USER_QUERY, data }); }; saveToState = e => { @@ -27,46 +31,46 @@ class Signin extends Component { }; render() { + // TODO / ASK - do I really have to wrap this in a query just to access the refetch function return ( - <Mutation - update={this.update} - mutation={SIGNIN_MUTATION} - variables={this.state} - refetchQueries={[{ query: CURRENT_USER_QUERY }]} - > - {(signin, { data, loading, error }) => ( - <Form onSubmit={e => this.loginUser(e, signin)}> - <Error error={error} /> - <fieldset disabled={loading} aria-busy={loading}> - <label htmlFor="email"> - Email - <input - value={this.state.email} - onChange={this.saveToState} - name="email" - type="text" - placeholder="email" - /> - </label> + <Query query={CURRENT_USER_QUERY}> + {({ refetch }) => ( + <Mutation mutation={SIGNIN_MUTATION} variables={this.state}> + {(signin, { data, loading, error }) => ( + <Form onSubmit={e => this.loginUser(e, signin, refetch)}> + <Error error={error} /> + <fieldset disabled={loading} aria-busy={loading}> + <label htmlFor="email"> + Email + <input + value={this.state.email} + onChange={this.saveToState} + name="email" + type="text" + placeholder="email" + /> + </label> - <label htmlFor="password"> - Password - <input - type="password" - name="password" - id="password" - className="password" - placeholder="password" - value={this.state.password} - onChange={this.saveToState} - /> - </label> + <label htmlFor="password"> + Password + <input + type="password" + name="password" + id="password" + className="password" + placeholder="password" + value={this.state.password} + onChange={this.saveToState} + /> + </label> - <button type="submit">Sign In!</button> - </fieldset> - </Form> + <button type="submit">Sign In!</button> + </fieldset> + </Form> + )} + </Mutation> )} - </Mutation> + </Query> ); } } diff --git a/frontend/components/SingleItem.js b/frontend/components/SingleItem.js index d4bec4f..53d5b9f 100644 --- a/frontend/components/SingleItem.js +++ b/frontend/components/SingleItem.js @@ -1,18 +1,47 @@ import { Query } from 'react-apollo'; import PropTypes from 'prop-types'; import { SINGLE_ITEM_QUERY } from '../queries/index'; +import Dump from './Dump'; +import styled from 'styled-components'; +import Link from 'next/link'; +import Error from './ErrorMessage'; + +const SingleItemStyles = styled.div` + max-width: 1200px; + margin: 2rem auto; + box-shadow: ${props => props.theme.bs}; + img { + width: 100%; + object-fit: cover; + } + .details { + margin: 3rem; + font-size: 3rem; + } +`; const SingleItem = props => ( <Query query={SINGLE_ITEM_QUERY} variables={{ id: props.id }}> {({ data: { items }, loading, error }) => { - if (loading || error) return null; + if (loading) return <p>Loading...</p>; const [item] = items; return ( - <div> + <SingleItemStyles> + <Error error={error} /> <img src={item.largeImage || item.image} alt={item.title} /> - <h2>Viewing {item.title}</h2> - <p>{item.description}</p> - </div> + <div className="details"> + <h2>Viewing {item.title}</h2> + <p>{item.description}</p> + <Link + href={{ + pathname: '/admin/update', + query: { id: item.id }, + }} + > + <a>Edit ✏️</a> + </Link> + </div> + </SingleItemStyles> ); }} </Query> diff --git a/frontend/components/styles/SickButton.js b/frontend/components/styles/SickButton.js new file mode 100644 index 0000000..3bd5001 --- /dev/null +++ b/frontend/components/styles/SickButton.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const SickButton = styled.button` + background: red; + color: white; + font-weight: 500; + border: 0; + border-radius: 0; + text-transform: uppercase; + font-size: 2rem; + padding: 0.8rem 1.5rem; + transform: skew(-2deg); +`; + +export default SickButton; |
