├── client ├── static │ ├── fonts │ │ ├── Guttenbg.ttf │ │ ├── MankSans.ttf │ │ └── MankSans-Medium.ttf │ ├── images │ │ ├── favicon.png │ │ └── placeholder_large.jpg │ └── nprogress.css ├── pages │ ├── index.js │ ├── signup.js │ ├── sell.js │ ├── reset.js │ ├── _document.js │ ├── _app.js │ ├── permissions.js │ ├── buy.js │ ├── order.js │ ├── product │ │ ├── add.js │ │ ├── edit.js │ │ └── selections.js │ ├── orders.js │ └── sales.js ├── __tests__ │ ├── components │ │ ├── __snapshots__ │ │ │ ├── Order.test.js.snap │ │ │ ├── Permissions.test.js.snap │ │ │ ├── CartItem.test.js.snap │ │ │ └── OrdersList.test.js.snap │ │ ├── Forms │ │ │ ├── __snapshots__ │ │ │ │ ├── SigninForm.test.js.snap │ │ │ │ ├── SignupForm.test.js.snap │ │ │ │ ├── CreateProductForm.test.js.snap │ │ │ │ ├── ResetPasswordForm.test.js.snap │ │ │ │ ├── UpdateProductForm.test.js.snap │ │ │ │ ├── CreateProductVariantForm.test.js.snap │ │ │ │ └── UpdateProductVariantForm.test.js.snap │ │ │ ├── SigninForm.test.js │ │ │ └── SignupForm.test.js │ │ ├── Buttons │ │ │ ├── __snapshots__ │ │ │ │ ├── RequestPasswordReset.test.js.snap │ │ │ │ ├── Logout.test.js.snap │ │ │ │ ├── UpdateCartItem.test.js.snap │ │ │ │ ├── DeleteProduct.test.js.snap │ │ │ │ ├── AddToCart.test.js.snap │ │ │ │ ├── DeleteProductVariant.test.js.snap │ │ │ │ └── RemoveFromCart.test.js.snap │ │ │ ├── UpdatePermissions.test.js │ │ │ ├── UpdateCartItem.test.js │ │ │ ├── RemoveFromCart.test.js │ │ │ ├── Logout.test.js │ │ │ ├── CheckoutCart.test.js │ │ │ ├── RequestPasswordReset.test.js │ │ │ ├── DeleteProductVariant.test.js │ │ │ └── DeleteProduct.test.js │ │ ├── OrdersList.test.js │ │ ├── PageTitle.test.js │ │ ├── SalesList.test.js │ │ ├── ProductsList.test.js │ │ ├── Order.test.js │ │ ├── Product.test.js │ │ ├── SvgIcon.test.js │ │ ├── CartItem.test.js │ │ └── RequireSignIn.test.js │ ├── sample.test.js │ └── mocking.test.js ├── graphql │ ├── Mutation │ │ ├── local.js │ │ ├── order.js │ │ ├── image.js │ │ ├── index.js │ │ ├── cartItem.js │ │ ├── product.js │ │ ├── user.js │ │ └── variant.js │ ├── Query │ │ ├── index.js │ │ ├── local.js │ │ ├── user.js │ │ └── order.js │ └── index.js ├── jest.setup.js ├── components │ ├── User.js │ ├── Forms │ │ ├── index.js │ │ ├── SigninForm.js │ │ ├── SignupForm.js │ │ ├── ResetPasswordForm.js │ │ └── CreateProductForm.js │ ├── Header │ │ ├── Nav.js │ │ ├── Search.js │ │ ├── index.js │ │ └── Menu.js │ ├── Buttons │ │ ├── index.js │ │ ├── ToggleCart.js │ │ ├── Logout.js │ │ ├── UpdatePermissions.js │ │ ├── AddToCart.js │ │ ├── RequestPasswordReset.js │ │ ├── UpdateCartItem.js │ │ ├── RemoveFromCart.js │ │ ├── DeleteProductVariant.js │ │ ├── DeleteProduct.js │ │ └── CheckoutCart.js │ ├── ByCreator.js │ ├── SingleProduct.js │ ├── PriceTag.js │ ├── CartCount.js │ ├── Filter │ │ ├── FilterList.js │ │ ├── FilterSection.js │ │ └── FilterRange.js │ ├── RequireSignin.js │ ├── NotFound.js │ ├── SvgIcon.js │ ├── PageTitle.js │ ├── DisplayMessage.js │ ├── styles │ │ └── CartStyles.js │ ├── Permissions.js │ ├── OrdersList.js │ ├── Page.js │ ├── Product.js │ ├── EditProductVariants.js │ └── ProductsList.js ├── lib │ ├── test-utils │ │ ├── mocks │ │ │ ├── resolvers │ │ │ │ ├── image.js │ │ │ │ ├── local.js │ │ │ │ ├── index.js │ │ │ │ ├── cartItem.js │ │ │ │ └── variant.js │ │ │ ├── index.js │ │ │ └── typeDefs.js │ │ └── utils.js │ ├── cloudinary.js │ ├── init-apollo.js │ └── with-apollo-client.js └── README.md ├── server ├── src │ ├── resolvers │ │ ├── index.js │ │ ├── Query.js │ │ └── Mutation │ │ │ ├── index.js │ │ │ ├── image.js │ │ │ ├── order.js │ │ │ └── cartItem.js │ ├── mail.js │ ├── utils.js │ ├── schema.graphql │ └── index.js ├── .graphqlconfig.yml ├── prisma │ ├── prisma.yml │ └── datamodel.prisma ├── .env_example └── package.json ├── .gitignore ├── .travis.yml └── LICENSE /client/static/fonts/Guttenbg.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Answart/next-store/HEAD/client/static/fonts/Guttenbg.ttf -------------------------------------------------------------------------------- /client/static/fonts/MankSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Answart/next-store/HEAD/client/static/fonts/MankSans.ttf -------------------------------------------------------------------------------- /client/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Answart/next-store/HEAD/client/static/images/favicon.png -------------------------------------------------------------------------------- /client/pages/index.js: -------------------------------------------------------------------------------- 1 | const Home = () => ( 2 |
3 |

Home Page

4 |
5 | ); 6 | 7 | export default Home; 8 | -------------------------------------------------------------------------------- /client/static/fonts/MankSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Answart/next-store/HEAD/client/static/fonts/MankSans-Medium.ttf -------------------------------------------------------------------------------- /client/static/images/placeholder_large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Answart/next-store/HEAD/client/static/images/placeholder_large.jpg -------------------------------------------------------------------------------- /client/__tests__/components/__snapshots__/Order.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches the snap shot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /server/src/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const Query = require('./Query') 2 | const Mutation = require('./Mutation') 3 | 4 | 5 | module.exports = { 6 | Query, 7 | Mutation, 8 | }; 9 | -------------------------------------------------------------------------------- /client/__tests__/components/__snapshots__/Permissions.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches with snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/SigninForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/SignupForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/CreateProductForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/ResetPasswordForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/UpdateProductForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/RequestPasswordReset.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/__snapshots__/CartItem.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` when loading w/variant renders all extra components properly 1`] = `null`; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | **/.DS_Store 4 | *.log 5 | .next/ 6 | .build/ 7 | dist 8 | layout.md 9 | .env 10 | now.json 11 | .graphcoolrc 12 | package-lock.json 13 | .coveralls.yml 14 | coverage 15 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/CreateProductVariantForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/__tests__/components/Forms/__snapshots__/UpdateProductVariantForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /client/graphql/Mutation/local.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const TOGGLE_LOCAL_CARTOPEN_MUTATION = gql` 5 | mutation TOGGLE_LOCAL_CARTOPEN_MUTATION { 6 | toggleCart @client 7 | } 8 | `; 9 | 10 | 11 | export { 12 | TOGGLE_LOCAL_CARTOPEN_MUTATION 13 | }; 14 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/Logout.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches the snap shot 1`] = ` 4 | 10 | `; 11 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/UpdateCartItem.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = ` 4 | 10 | `; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '11' 5 | - stable 6 | cache: 7 | directories: 8 | - client/node_modules 9 | - server/node_modules 10 | script: 11 | - cd client && npm install && npm run build && npm test 12 | after_script: cd client && npm run coveralls 13 | -------------------------------------------------------------------------------- /client/jest.setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | 6 | window.alert = jest.fn(); 7 | window.confirm = jest.fn(() => true); 8 | window.matchMedia = () => ({}); 9 | window.scrollTo = () => { }; 10 | -------------------------------------------------------------------------------- /client/graphql/Mutation/order.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const CREATE_ORDER_MUTATION = gql` 5 | mutation CREATE_ORDER_MUTATION($token: String!) { 6 | createOrder(token: $token) { 7 | id 8 | } 9 | } 10 | `; 11 | 12 | 13 | export { 14 | CREATE_ORDER_MUTATION 15 | } 16 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/DeleteProduct.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = ` 4 | 11 | `; 12 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/AddToCart.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches the snap shot 1`] = ` 4 | 12 | `; 13 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/DeleteProductVariant.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = ` 4 | 11 | `; 12 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/__snapshots__/RemoveFromCart.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = ` 4 | 13 | `; 14 | -------------------------------------------------------------------------------- /client/graphql/Query/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | LOCAL_CARTOPEN_QUERY, 3 | LOCAL_USER_QUERY, 4 | } from './local'; 5 | export { 6 | ALL_USERS_QUERY, 7 | CURRENT_USER_QUERY, 8 | } from './user'; 9 | export { 10 | ORDER_QUERY, 11 | ORDERS_QUERY, 12 | ORDER_ITEMS_QUERY, 13 | } from './order'; 14 | export { 15 | PRODUCT_QUERY, 16 | SHOP_PRODUCTS_QUERY, 17 | PAGINATION_QUERY, 18 | } from './product'; 19 | -------------------------------------------------------------------------------- /client/components/User.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Query } from 'react-apollo'; 3 | import { LOCAL_USER_QUERY } from '../graphql'; 4 | 5 | 6 | const User = ({ children }) => ( 7 | 8 | {payload => children(payload)} 9 | 10 | ); 11 | 12 | User.propTypes = { 13 | children: PropTypes.func.isRequired 14 | }; 15 | 16 | 17 | export default User; 18 | -------------------------------------------------------------------------------- /client/components/Forms/index.js: -------------------------------------------------------------------------------- 1 | export { SignupForm } from './SignupForm.js'; 2 | export { SigninForm } from './SigninForm.js'; 3 | export { ResetPasswordForm } from './ResetPasswordForm.js'; 4 | export { CreateProductForm } from './CreateProductForm.js'; 5 | export { CreateProductVariantForm } from './CreateProductVariantForm.js'; 6 | export { UpdateProductForm } from './UpdateProductForm.js'; 7 | export { UpdateProductVariantForm } from './UpdateProductVariantForm.js'; 8 | -------------------------------------------------------------------------------- /server/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | app: 3 | schemaPath: src/schema.graphql 4 | includes: [ 5 | "schema.graphql" 6 | "prisma.graphql" 7 | ] 8 | extensions: 9 | endpoints: 10 | default: ${env:HOST}${env:PORT} 11 | prisma: 12 | schemaPath: src/generated/prisma.graphql 13 | includes: [ 14 | "prisma.graphql", 15 | "datamodel.graphql" 16 | ] 17 | extensions: 18 | prisma: prisma/prisma.yml 19 | -------------------------------------------------------------------------------- /client/pages/signup.js: -------------------------------------------------------------------------------- 1 | import { StyledSignupPage } from '../components/styles/PageStyles'; 2 | import PageTitle from '../components/PageTitle'; 3 | import { SignupForm, SigninForm } from '../components/Forms'; 4 | 5 | 6 | const SignupPage = () => ( 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 |
15 | ); 16 | 17 | 18 | export default SignupPage; 19 | -------------------------------------------------------------------------------- /client/components/Header/Nav.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { DEPARTMENTS } from '../../config'; 3 | import { capWord } from '../../lib/utils'; 4 | 5 | 6 | const Nav = () => ( 7 |
8 | {DEPARTMENTS.map(dept => ( 9 | 13 | 14 | {capWord(dept)} 15 | 16 | 17 | ))} 18 |
19 | ); 20 | 21 | 22 | export default Nav; 23 | -------------------------------------------------------------------------------- /client/components/Buttons/index.js: -------------------------------------------------------------------------------- 1 | export { AddToCart } from './AddToCart'; 2 | export { CheckoutCart } from './CheckoutCart'; 3 | export { DeleteProduct } from './DeleteProduct'; 4 | export { DeleteProductVariant } from './DeleteProductVariant'; 5 | export { Logout } from './Logout'; 6 | export { RemoveFromCart } from './RemoveFromCart'; 7 | export { RequestPasswordReset } from './RequestPasswordReset'; 8 | export { ToggleCart } from './ToggleCart'; 9 | export { UpdateCartItem } from './UpdateCartItem'; 10 | export { UpdatePermissions } from './UpdatePermissions'; 11 | -------------------------------------------------------------------------------- /client/components/ByCreator.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import Link from 'next/link'; 3 | 4 | 5 | const ByCreator = ({ name, online }) => ( 6 |
7 | By 8 | 12 | {name} 13 | 14 |
15 | ); 16 | 17 | ByCreator.propTypes = { 18 | name: PropTypes.string.isRequired, 19 | online: PropTypes.bool.isRequired 20 | }; 21 | 22 | 23 | export default ByCreator; 24 | -------------------------------------------------------------------------------- /client/components/SingleProduct.js: -------------------------------------------------------------------------------- 1 | import { Query } from 'react-apollo'; 2 | import PropTypes from 'prop-types'; 3 | import { PRODUCT_QUERY } from '../graphql'; 4 | 5 | 6 | const SingleItem = ({ variables, children }) => ( 7 | 8 | {payload => children(payload)} 9 | 10 | ); 11 | 12 | SingleItem.propTypes = { 13 | variables: PropTypes.shape({ 14 | id: PropTypes.string 15 | }).isRequired, 16 | children: PropTypes.func.isRequired 17 | }; 18 | 19 | 20 | export default SingleItem; 21 | -------------------------------------------------------------------------------- /client/__tests__/sample.test.js: -------------------------------------------------------------------------------- 1 | describe('sample test 101', () => { 2 | it('works as expected', () => { 3 | const age = 100; 4 | expect(1).toEqual(1); 5 | expect(age).toEqual(100); 6 | }); 7 | 8 | it('handles ranges just fine', () => { 9 | const age = 200; 10 | expect(age).toBeGreaterThan(100); 11 | }); 12 | 13 | it('makes a list of dog names', () => { 14 | const dogs = ['snickers', 'hugo']; 15 | expect(dogs).toEqual(dogs); 16 | expect(dogs).toContain('snickers'); 17 | expect(dogs).toContain('snickers'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /client/components/Buttons/ToggleCart.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Mutation } from 'react-apollo'; 3 | import { TOGGLE_LOCAL_CARTOPEN_MUTATION } from '../../graphql'; 4 | 5 | 6 | const ToggleCart = props => ( 7 | 8 | {(toggleCart) => ( 9 | 12 | )} 13 | 14 | ); 15 | 16 | ToggleCart.propTypes = { 17 | children: PropTypes.object.isRequired 18 | }; 19 | 20 | 21 | export { ToggleCart }; 22 | -------------------------------------------------------------------------------- /client/pages/sell.js: -------------------------------------------------------------------------------- 1 | import { StyledCreatePage } from '../components/styles/PageStyles'; 2 | import { CreateProductForm } from '../components/Forms'; 3 | import PageTitle from '../components/PageTitle'; 4 | import RequireSignin from '../components/RequireSignin'; 5 | 6 | 7 | const CreateProductPage = () => ( 8 | 9 | 10 | 11 |
12 | 13 | {({ me }) => ( 14 | 15 | )} 16 | 17 |
18 |
19 | ); 20 | 21 | 22 | export default CreateProductPage; 23 | -------------------------------------------------------------------------------- /client/pages/reset.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import PageTitle from '../components/PageTitle'; 3 | import { ResetPasswordForm } from '../components/Forms'; 4 | import { StyledCreatePage } from '../components/styles/PageStyles'; 5 | 6 | 7 | const ResetPasswordPage = ({ query }) => ( 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 | ); 16 | 17 | ResetPasswordPage.propTypes = { 18 | query: PropTypes.shape({ 19 | resetToken: PropTypes.string 20 | }).isRequired 21 | }; 22 | 23 | 24 | export default ResetPasswordPage; 25 | -------------------------------------------------------------------------------- /client/graphql/Mutation/image.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const CREATE_IMAGE_MUTATION = gql` 5 | mutation CREATE_IMAGE_MUTATION( 6 | $cloudinary_id: String!, 7 | $name: String!, 8 | $height: Int!, 9 | $width: Int!, 10 | $transformation: String!, 11 | $image_url: String!, 12 | $large_image_url: String! 13 | ) { 14 | createImage( 15 | cloudinary_id: $cloudinary_id, 16 | name: $name, 17 | height: $height, 18 | width: $width, 19 | transformation: $transformation, 20 | image_url: $image_url, 21 | large_image_url: $large_image_url 22 | ) { 23 | id 24 | } 25 | } 26 | `; 27 | 28 | 29 | export { 30 | CREATE_IMAGE_MUTATION 31 | } 32 | -------------------------------------------------------------------------------- /client/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | import { ServerStyleSheet } from 'styled-components'; 3 | 4 | 5 | export default class MyDocument extends Document { 6 | static getInitialProps({ renderPage }) { 7 | const sheet = new ServerStyleSheet(); 8 | const page = renderPage(App => props => sheet.collectStyles()); 9 | const styleTags = sheet.getStyleElement(); 10 | return { ...page, styleTags }; 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | {this.props.styleTags} 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/components/Buttons/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Router from 'next/router'; 4 | import { Mutation } from 'react-apollo'; 5 | import { SIGNOUT_MUTATION, CURRENT_USER_QUERY } from '../../graphql'; 6 | 7 | 8 | const Logout = () => ( 9 | 12 | {(signout) => ( 13 | 24 | )} 25 | 26 | ); 27 | 28 | 29 | export { Logout }; 30 | -------------------------------------------------------------------------------- /client/graphql/Mutation/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | TOGGLE_LOCAL_CARTOPEN_MUTATION, 3 | } from './local'; 4 | export { 5 | SIGNUP_MUTATION, 6 | SIGNIN_MUTATION, 7 | SIGNOUT_MUTATION, 8 | REQUEST_PASSWORD_RESET_MUTATION, 9 | RESET_PASSWORD_MUTATION, 10 | UPDATE_PERMISSIONS_MUTATION, 11 | } from './user'; 12 | export { 13 | CREATE_IMAGE_MUTATION, 14 | } from './image'; 15 | export { 16 | CREATE_PRODUCT_MUTATION, 17 | UPDATE_PRODUCT_MUTATION, 18 | DELETE_PRODUCT_MUTATION, 19 | } from './product'; 20 | export { 21 | CREATE_PROD_VARIANT_MUTATION, 22 | UPDATE_PROD_VARIANT_MUTATION, 23 | DELETE_PROD_VARIANT_MUTATION, 24 | } from './variant'; 25 | export { 26 | ADD_TO_CART_MUTATION, 27 | UPDATE_CARTITEM_MUTATION, 28 | REMOVE_FROM_CART_MUTATION, 29 | } from './cartItem'; 30 | export { 31 | CREATE_ORDER_MUTATION, 32 | } from './order'; 33 | -------------------------------------------------------------------------------- /client/graphql/Mutation/cartItem.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const ADD_TO_CART_MUTATION = gql` 5 | mutation ADD_TO_CART_MUTATION($id: ID!) { 6 | addToCart(id: $id) { 7 | id 8 | } 9 | } 10 | `; 11 | 12 | const UPDATE_CARTITEM_MUTATION = gql` 13 | mutation UPDATE_CARTITEM_MUTATION( 14 | $id: ID!, 15 | $quantity: Int! 16 | ) { 17 | updateCartItem( 18 | id: $id, 19 | quantity: $quantity 20 | ) { 21 | id 22 | quantity 23 | variant { 24 | id 25 | quantity 26 | } 27 | } 28 | } 29 | `; 30 | 31 | const REMOVE_FROM_CART_MUTATION = gql` 32 | mutation REMOVE_FROM_CART_MUTATION($id: ID!) { 33 | removeFromCart(id: $id) { 34 | id 35 | } 36 | } 37 | `; 38 | 39 | 40 | export { 41 | ADD_TO_CART_MUTATION, 42 | UPDATE_CARTITEM_MUTATION, 43 | REMOVE_FROM_CART_MUTATION 44 | } 45 | -------------------------------------------------------------------------------- /client/graphql/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | TOGGLE_LOCAL_CARTOPEN_MUTATION, 3 | SIGNUP_MUTATION, 4 | SIGNIN_MUTATION, 5 | SIGNOUT_MUTATION, 6 | REQUEST_PASSWORD_RESET_MUTATION, 7 | RESET_PASSWORD_MUTATION, 8 | UPDATE_PERMISSIONS_MUTATION, 9 | CREATE_IMAGE_MUTATION, 10 | CREATE_PRODUCT_MUTATION, 11 | UPDATE_PRODUCT_MUTATION, 12 | DELETE_PRODUCT_MUTATION, 13 | CREATE_PROD_VARIANT_MUTATION, 14 | UPDATE_PROD_VARIANT_MUTATION, 15 | DELETE_PROD_VARIANT_MUTATION, 16 | ADD_TO_CART_MUTATION, 17 | UPDATE_CARTITEM_MUTATION, 18 | REMOVE_FROM_CART_MUTATION, 19 | CREATE_ORDER_MUTATION, 20 | } from './Mutation'; 21 | export { 22 | LOCAL_CARTOPEN_QUERY, 23 | LOCAL_USER_QUERY, 24 | ALL_USERS_QUERY, 25 | CURRENT_USER_QUERY, 26 | ORDER_QUERY, 27 | ORDERS_QUERY, 28 | ORDER_ITEMS_QUERY, 29 | PRODUCT_QUERY, 30 | SHOP_PRODUCTS_QUERY, 31 | PAGINATION_QUERY, 32 | } from './Query'; 33 | -------------------------------------------------------------------------------- /client/graphql/Query/local.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const LOCAL_CARTOPEN_QUERY = gql` 5 | query LOCAL_CARTOPEN_QUERY { 6 | cartOpen @client 7 | } 8 | `; 9 | 10 | const LOCAL_USER_QUERY = gql` 11 | query LOCAL_USER_QUERY { 12 | me @client { 13 | id 14 | email 15 | name 16 | cart { 17 | id 18 | quantity 19 | user { 20 | id 21 | name 22 | } 23 | variant { 24 | id 25 | size 26 | color 27 | quantity 28 | price 29 | sale 30 | salePrice 31 | product { 32 | id 33 | title 34 | } 35 | image { 36 | id 37 | image_url 38 | } 39 | } 40 | } 41 | } 42 | } 43 | `; 44 | 45 | 46 | export { 47 | LOCAL_CARTOPEN_QUERY, 48 | LOCAL_USER_QUERY, 49 | }; 50 | -------------------------------------------------------------------------------- /server/prisma/prisma.yml: -------------------------------------------------------------------------------- 1 | # The HTTP endpoint for your Prisma API 2 | endpoint: ${env:PRISMA_DEV_ENDPOINT} 3 | # endpoint: ${env:PRISMA_PROD_ENDPOINT} 4 | 5 | # Points to the file that holds your data model 6 | datamodel: datamodel.prisma 7 | 8 | # You can only access the API when providing JWTs that are signed with this secret 9 | # The secret is used to generate JTWs which allow to authenticate 10 | # against your Prisma service. You can use the `prisma token` command from the CLI 11 | # to generate a JWT based on the secret. When using the `prisma-binding` package, 12 | # you don't need to generate the JWTs manually as the library is doing that for you 13 | # (this is why you're passing it to the `Prisma` constructor). 14 | secret: ${env:PRISMA_SECRET} 15 | 16 | generate: 17 | - generator: graphql-schema 18 | output: ../src/generated/ 19 | 20 | hooks: 21 | post-deploy: 22 | - prisma generate 23 | -------------------------------------------------------------------------------- /client/components/PriceTag.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import styled from 'styled-components'; 3 | import { formatMoney } from '../lib/utils'; 4 | 5 | 6 | const StyledPriceTag = styled.div` 7 | .price-tag-padding { 8 | padding: 1rem 0.5rem; 9 | } 10 | .price-tag-priceSale { 11 | color: ${props => props.theme.red}; 12 | } 13 | `; 14 | 15 | const PriceTag = ({ price, sale, salePrice }) => ( 16 | 17 | 18 | {formatMoney(price)} 19 | 20 | 21 | {sale && ( 22 | 23 | {formatMoney(salePrice)} 24 | 25 | )} 26 | 27 | ); 28 | 29 | PriceTag.propTypes = { 30 | price: PropTypes.number.isRequired, 31 | sale: PropTypes.bool.isRequired, 32 | salePrice: PropTypes.number 33 | }; 34 | 35 | 36 | export default PriceTag; 37 | -------------------------------------------------------------------------------- /client/lib/test-utils/mocks/resolvers/image.js: -------------------------------------------------------------------------------- 1 | import { CREATE_IMAGE_MUTATION } from '../../../../graphql' 2 | import { mockImage, mockImageVariables } from '../typeDefs'; 3 | 4 | 5 | const createImageMutationMock = (overrides) => ({ 6 | request: { 7 | query: CREATE_IMAGE_MUTATION, 8 | variables: { 9 | ...mockImageVariables, 10 | ...overrides, 11 | }, 12 | }, 13 | result: { 14 | data: { 15 | createImage: { 16 | __typename: 'Image', 17 | id: mockImage.id, 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | const createImageMutationErrorMock = { 24 | request: { 25 | query: CREATE_IMAGE_MUTATION, 26 | variables: { 27 | ...mockImageVariables, 28 | cloudinary_id: '', 29 | name: '', 30 | }, 31 | }, 32 | result: { 33 | errors: [{ message: 'ack!' }], 34 | } 35 | }; 36 | 37 | 38 | export { 39 | createImageMutationMock, createImageMutationErrorMock, 40 | } 41 | -------------------------------------------------------------------------------- /client/components/CartCount.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { TransitionGroup, CSSTransition } from 'react-transition-group'; 4 | import { StyledCartCountAnimation, StyledCartCountDot } from './styles/CartStyles'; 5 | 6 | 7 | const CartCount = ({ count }) => ( 8 | 9 | 10 | 17 | 18 |
19 | {count} 20 |
21 |
22 |
23 |
24 |
25 | ); 26 | 27 | CartCount.propTypes = { 28 | count: PropTypes.number.isRequired, 29 | }; 30 | 31 | 32 | export default CartCount; 33 | -------------------------------------------------------------------------------- /client/graphql/Query/user.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const ALL_USERS_QUERY = gql` 5 | query ALL_USERS_QUERY { 6 | users { 7 | id 8 | name 9 | email 10 | permissions 11 | } 12 | } 13 | `; 14 | 15 | const CURRENT_USER_QUERY = gql` 16 | query CURRENT_USER_QUERY { 17 | me { 18 | id 19 | email 20 | name 21 | cart { 22 | id 23 | quantity 24 | user { 25 | id 26 | name 27 | } 28 | variant { 29 | id 30 | size 31 | color 32 | quantity 33 | price 34 | sale 35 | salePrice 36 | product { 37 | id 38 | title 39 | } 40 | image { 41 | id 42 | image_url 43 | } 44 | } 45 | } 46 | } 47 | } 48 | `; 49 | 50 | 51 | export { 52 | ALL_USERS_QUERY, 53 | CURRENT_USER_QUERY, 54 | }; 55 | -------------------------------------------------------------------------------- /client/components/Buttons/UpdatePermissions.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Mutation } from 'react-apollo'; 3 | import { UPDATE_PERMISSIONS_MUTATION, CURRENT_USER_QUERY } from '../../graphql'; 4 | 5 | 6 | const UpdatePermissions = ({ permissions, userId }) => ( 7 | 10 | {(updatePermissions, { loading, error }) => ( 11 | 21 | )} 22 | 23 | ); 24 | 25 | UpdatePermissions.propTypes = { 26 | userId: PropTypes.string.isRequired, 27 | permissions: PropTypes.array.isRequired, 28 | }; 29 | 30 | 31 | export { UpdatePermissions }; 32 | -------------------------------------------------------------------------------- /client/lib/test-utils/mocks/resolvers/local.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOCAL_CARTOPEN_QUERY, 3 | LOCAL_USER_QUERY, 4 | } from '../../../../graphql' 5 | import { mockUser, mockCartItem } from '../typeDefs'; 6 | 7 | 8 | const localUserQueryEmptyCartMock = { 9 | request: { query: LOCAL_USER_QUERY }, 10 | result: { 11 | data: { 12 | me: { 13 | ...mockUser, 14 | cart: [], 15 | }, 16 | }, 17 | }, 18 | }; 19 | 20 | const localUserQueryNoUserMock = { 21 | request: { query: LOCAL_USER_QUERY }, 22 | result: { 23 | data: { me: null } 24 | }, 25 | }; 26 | 27 | const localUserQueryCartItemMock = overrides => ({ 28 | request: { query: LOCAL_USER_QUERY }, 29 | result: { 30 | data: { 31 | me: { 32 | ...mockUser, 33 | cart: [{ 34 | ...mockCartItem, 35 | ...overrides 36 | }], 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | 43 | export { 44 | localUserQueryEmptyCartMock, localUserQueryNoUserMock, localUserQueryCartItemMock, 45 | }; 46 | -------------------------------------------------------------------------------- /server/.env_example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | APP_SECRET= 3 | HOST=http://localhost 4 | PORT=4242 5 | DEV_CLIENT_URL=http://localhost:7272 6 | PROD_CLIENT_URL= 7 | PROD_SERVER_URL= 8 | CLOUDINARY_API_KEY= 9 | CLOUDINARY_PRESET=nextstore 10 | CLOUDINARY_SECRET= 11 | PRISMA_DEV_ENDPOINT=https://.sh///dev 12 | PRISMA_PROD_ENDPOINT=https://.herokuapp.com//prod 13 | PRISMA_SECRET= 14 | PRISMA_MANAGEMENT_API_SECRET= 15 | STRIPE_SECRET= 16 | MAILTRAP_HOST= 17 | MAILTRAP_PORT= 18 | MAILTRAP_USER= 19 | MAILTRAP_PASS= 20 | POSTMARK_HOST= 21 | POSTMARK_PORT= 22 | POSTMARK_USER= 23 | POSTMARK_PASS= 24 | -------------------------------------------------------------------------------- /client/components/Buttons/AddToCart.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Mutation } from 'react-apollo'; 3 | import { ADD_TO_CART_MUTATION, CURRENT_USER_QUERY } from '../../graphql'; 4 | 5 | 6 | const AddToCart = ({ variant, disabled }) => ( 7 | 11 | {(addToCart, { loading }) => ( 12 | 22 | )} 23 | 24 | ); 25 | 26 | AddToCart.propTypes = { 27 | variant: PropTypes.shape({ 28 | id: PropTypes.string.isRequired 29 | }).isRequired, 30 | disabled: PropTypes.bool.isRequired 31 | }; 32 | 33 | 34 | export { AddToCart }; 35 | -------------------------------------------------------------------------------- /client/components/Filter/FilterList.js: -------------------------------------------------------------------------------- 1 | import { capWord } from '../../lib/utils'; 2 | 3 | 4 | const FilterList = (props) => { 5 | if (!props.list && !props.list.length) return; 6 | const { name, currentFilter } = props; 7 | return ( 8 |
9 | {props.list.map((listItem, i) => ( 10 | 23 | ))} 24 |
25 | ) 26 | }; 27 | 28 | 29 | export default FilterList; 30 | -------------------------------------------------------------------------------- /server/src/resolvers/Query.js: -------------------------------------------------------------------------------- 1 | const { forwardTo } = require('prisma-binding'); 2 | const { hasPermission } = require('../utils'); 3 | 4 | 5 | const Query = { 6 | async me(parent, args, ctx, info) { 7 | const id = ctx.request.userId; 8 | if (!id) return null; 9 | 10 | return await ctx.db.query.user({ 11 | where: { id }, 12 | }, info); 13 | }, 14 | async users(parent, args, ctx, info) { 15 | if (!ctx.request.userId) throw new Error('USERS: You must be logged in!'); 16 | 17 | // requester has permission to query all users? 18 | hasPermission(ctx.request.user, ['ADMIN', 'PERMISSIONUPDATE']) 19 | 20 | return await ctx.db.query.users({}, info); 21 | }, 22 | image: forwardTo('db'), 23 | images: forwardTo('db'), 24 | product: forwardTo('db'), 25 | products: forwardTo('db'), 26 | productsConnection: forwardTo('db'), 27 | variant: forwardTo('db'), 28 | variants: forwardTo('db'), 29 | order: forwardTo('db'), 30 | orders: forwardTo('db'), 31 | orderItems: forwardTo('db'), 32 | }; 33 | 34 | 35 | module.exports = Query; 36 | -------------------------------------------------------------------------------- /client/components/Buttons/RequestPasswordReset.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Mutation } from 'react-apollo'; 4 | import { REQUEST_PASSWORD_RESET_MUTATION } from '../../graphql'; 5 | 6 | 7 | const RequestPasswordReset = ({ email, children }) => ( 8 | 9 | {(requestPasswordReset) => ( 10 | 23 | )} 24 | 25 | ); 26 | 27 | RequestPasswordReset.propTypes = { 28 | email: PropTypes.string, 29 | children: PropTypes.string.isRequired 30 | }; 31 | 32 | 33 | export { RequestPasswordReset }; 34 | -------------------------------------------------------------------------------- /server/src/mail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | 3 | 4 | const options = (process.env.NODE_ENV === 'production') 5 | ? { 6 | host: process.env.POSTMARK_HOST, 7 | port: process.env.POSTMARK_PORT, 8 | auth: { 9 | user: process.env.POSTMARK_USER, 10 | pass: process.env.POSTMARK_PASS 11 | } 12 | } 13 | : { 14 | host: process.env.MAILTRAP_HOST, 15 | port: process.env.MAILTRAP_PORT, 16 | auth: { 17 | user: process.env.MAILTRAP_USER, 18 | pass: process.env.MAILTRAP_PASS 19 | } 20 | } 21 | 22 | const transport = nodemailer.createTransport({ ...options }); 23 | 24 | const emailFromNextStoreSupport = text => ` 25 |
32 |

Hello There!

33 |

${text}

34 | 35 |

- NextStore Support

36 |
37 | `; 38 | 39 | 40 | exports.transport = transport; 41 | exports.emailFromNextStoreSupport = emailFromNextStoreSupport; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexandra Swart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/src/resolvers/Mutation/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | createUser, 3 | signin, 4 | signout, 5 | requestPasswordReset, 6 | resetPassword, 7 | updatePermissions, 8 | } = require('./user'); 9 | const { 10 | createImage, 11 | deleteImage, 12 | } = require('./image'); 13 | const { 14 | createProduct, 15 | updateProduct, 16 | deleteProduct, 17 | } = require('./product'); 18 | const { 19 | createProductVariant, 20 | updateProductVariant, 21 | deleteProductVariant, 22 | } = require('./variant'); 23 | const { 24 | addToCart, 25 | updateCartItem, 26 | removeFromCart, 27 | } = require('./cartItem'); 28 | const { 29 | createOrder, 30 | } = require('./order'); 31 | 32 | 33 | 34 | const Mutation = { 35 | createUser, 36 | signin, 37 | signout, 38 | requestPasswordReset, 39 | resetPassword, 40 | updatePermissions, 41 | createImage, 42 | deleteImage, 43 | createProduct, 44 | updateProduct, 45 | deleteProduct, 46 | createProductVariant, 47 | updateProductVariant, 48 | deleteProductVariant, 49 | addToCart, 50 | removeFromCart, 51 | updateCartItem, 52 | createOrder, 53 | }; 54 | 55 | 56 | module.exports = Mutation; 57 | -------------------------------------------------------------------------------- /client/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import App, { Container as NextContainer } from 'next/app'; 3 | import { ApolloProvider } from 'react-apollo'; 4 | import Page from '../components/Page'; 5 | import withApolloClient from '../lib/with-apollo-client'; 6 | 7 | 8 | class NextApp extends App { 9 | static async getInitialProps({ Component, ctx }) { 10 | let pageProps = {}; 11 | if (!!Component.getInitialProps) { 12 | try { 13 | pageProps = await Component.getInitialProps(ctx); 14 | } catch(e) { 15 | console.error('Error: getInitialProps failed.', e) 16 | } 17 | } 18 | pageProps.query = ctx.query; 19 | 20 | return { pageProps }; 21 | } 22 | render() { 23 | const { Component, pageProps, apolloClient, apolloState } = this.props; 24 | 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | } 36 | 37 | 38 | export default withApolloClient(NextApp); 39 | -------------------------------------------------------------------------------- /client/__tests__/mocking.test.js: -------------------------------------------------------------------------------- 1 | function Person(name, foods) { 2 | this.name = name; 3 | this.foods = foods; 4 | } 5 | 6 | Person.prototype.fetchFavFoods = function() { 7 | return new Promise((resolve, reject) => { 8 | // Simulate an API 9 | setTimeout(() => resolve(this.foods), 2000); 10 | }); 11 | }; 12 | 13 | describe('mocking learning', () => { 14 | it('mocks a reg function', () => { 15 | const fetchDogs = jest.fn(); 16 | fetchDogs('snickers'); 17 | expect(fetchDogs).toHaveBeenCalled(); 18 | expect(fetchDogs).toHaveBeenCalledWith('snickers'); 19 | fetchDogs('hugo'); 20 | expect(fetchDogs).toHaveBeenCalledTimes(2); 21 | }); 22 | 23 | it('can create a person', () => { 24 | const me = new Person('Wes', ['pizza', 'burgs']); 25 | expect(me.name).toBe('Wes'); 26 | }); 27 | 28 | it('can fetch foods', async () => { 29 | const me = new Person('Wes', ['pizza', 'burgs']); 30 | // mock the favFoods function 31 | me.fetchFavFoods = jest.fn().mockResolvedValue(['sushi', 'ramen']); 32 | const favFoods = await me.fetchFavFoods(); 33 | // console.log(favFoods); 34 | expect(favFoods).toContain('sushi'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /client/components/Filter/FilterSection.js: -------------------------------------------------------------------------------- 1 | import SvgIcon from '../SvgIcon'; 2 | 3 | 4 | const FilterSection = (props) => { 5 | const { name, currentFilter } = props; 6 | return ( 7 |
8 |
9 | 17 | 18 | 26 | 27 | 33 |
34 | {props.children} 35 |
36 | ) 37 | } 38 | 39 | 40 | export default FilterSection; 41 | -------------------------------------------------------------------------------- /client/components/Header/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Router from 'next/router'; 3 | import SvgIcon from '../SvgIcon'; 4 | 5 | 6 | class Search extends Component { 7 | state = { title: '' }; 8 | saveToState = e => this.setState({ [e.target.name]: e.target.value }); 9 | search = (e) => { 10 | if (!!e && e.preventDefault) e.preventDefault(); 11 | if (!this.state.title.length) return; 12 | 13 | Router.push({ 14 | pathname: "/shop", 15 | query: { ...this.state } 16 | }); 17 | }; 18 | render() { 19 | const disabled = this.state.title 20 | ? !this.state.title.length 21 | : true; 22 | return ( 23 |
24 | 25 | 26 | 32 | 33 | 38 |
39 | ) 40 | } 41 | }; 42 | 43 | 44 | export default Search; 45 | -------------------------------------------------------------------------------- /client/__tests__/components/Buttons/UpdatePermissions.test.js: -------------------------------------------------------------------------------- 1 | import wait from 'waait'; 2 | import toJSON from 'enzyme-to-json'; 3 | import { mount } from 'enzyme'; 4 | import { MockedProvider } from 'react-apollo/test-utils'; 5 | import { mockUser, updatePermissionsMutationMock } from '../../../lib/test-utils/mocks'; 6 | import { UpdatePermissions } from '../../../components/Buttons'; 7 | 8 | const mocks = [ 9 | { ...updatePermissionsMutationMock } 10 | ]; 11 | 12 | 13 | describe('', () => { 14 | let wrapper; 15 | beforeAll(() => { 16 | wrapper = mount( 17 | 18 | 22 | 23 | ); 24 | }); 25 | afterAll(() => wrapper.unmount()); 26 | 27 | it('renders and matches snapshot', async () => { 28 | expect(toJSON(wrapper.find('UpdatePermissions'))).toMatchSnapshot(); 29 | }); 30 | 31 | it('click renders properly', async () => { 32 | wrapper.find('button').simulate('click'); 33 | expect(wrapper.find('button').text()).toContain('Updating'); 34 | await wait(80); 35 | wrapper.update(); 36 | expect(wrapper.find('button').text()).toContain('Update'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /client/components/RequireSignin.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Query } from 'react-apollo'; 3 | import { SigninForm } from './Forms'; 4 | import NotFound from './NotFound'; 5 | import User from './User'; 6 | import styled from 'styled-components'; 7 | 8 | const StyledRequireSignin = styled.div` 9 | display: grid; 10 | grid-template-rows: 2rem minmax(20rem, 30rem); 11 | grid-auto-flow: row; 12 | justify-content: center; 13 | grid-gap: 2rem; 14 | form { 15 | max-height: 32rem; 16 | } 17 | `; 18 | 19 | 20 | const RequireSignin = (props) => ( 21 | 22 | {({ loading, error, data }) => { 23 | if (loading) return (

Loading...

); 24 | if (error) return (); 25 | const me = (!data || !data.me) 26 | ? null 27 | : data.me; 28 | if (!me) { 29 | return ( 30 | 31 |

Please Sign In before Continuing

32 | 33 | 34 |
35 | ); 36 | } 37 | return props.children({ me }); 38 | }} 39 |
40 | ); 41 | 42 | RequireSignin.propTypes = { 43 | children: PropTypes.func.isRequired 44 | }; 45 | 46 | 47 | export default RequireSignin; 48 | -------------------------------------------------------------------------------- /client/lib/test-utils/mocks/resolvers/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | localUserQueryEmptyCartMock, localUserQueryNoUserMock, localUserQueryCartItemMock, 3 | } from './local'; 4 | export { 5 | userQueryEmptyCartMock, userQueryNoUserMock, userQueryCartItemMock, 6 | signupMutationMock, 7 | signinMutationMock, 8 | signoutMutationMock, 9 | requestPasswordResetMutationMock, requestPasswordResetMutationErrorMock, 10 | resetPasswordMutationMock, resetPasswordMutationErrorMock, 11 | updatePermissionsMutationMock, 12 | } from './user'; 13 | export { 14 | createImageMutationMock, createImageMutationErrorMock, 15 | } from './image'; 16 | export { 17 | productQueryMock, productQueryErrorMock, productQueryNoProductMock, productQueryNoVariantMock, 18 | shopProductsQueryFilterMock, 19 | createProductMutationMock, createProductMutationErrorMock, 20 | updateProductMutationMock, updateProductMutationErrorMock, 21 | deleteProductMutationMock, 22 | } from './product'; 23 | export { 24 | createProductVariantMutationMock, createProductVariantMutationErrorMock, 25 | updateProductVariantMutationMock, updateProductVariantMutationErrorMock, 26 | deleteProductVariantMutationMock 27 | } from './variant'; 28 | export { 29 | addToCartMutationMock, 30 | updateCartItemMutationMock, 31 | removeFromCartMutationMock 32 | } from './cartItem'; 33 | -------------------------------------------------------------------------------- /server/src/utils.js: -------------------------------------------------------------------------------- 1 | const SALES_TAX_RATE = 0.0925; 2 | const SHIPPING_COST_PER_ITEM = 1.2; 3 | 4 | 5 | function hasPermission(user, permissionsNeeded) { 6 | const matchedPermissions = user.permissions.filter(permissionTheyHave => 7 | permissionsNeeded.includes(permissionTheyHave) 8 | ); 9 | if (!matchedPermissions.length) { 10 | throw new Error(`You do not have sufficient permissions: ${permissionsNeeded}. You Have: ${user.permissions}`); 11 | } 12 | }; 13 | 14 | function getCartTotals(cart = []) { 15 | let quantity = 0; 16 | let shipping = 0; 17 | let tax = 0; 18 | let subtotal = 0; 19 | 20 | if (!!cart && !!cart.length) { 21 | quantity = cart.reduce((tally, cartItem) => { 22 | subtotal += cartItem.quantity * cartItem.price; 23 | shipping += cartItem.quantity * SHIPPING_COST_PER_ITEM; 24 | tax += cartItem.quantity * cartItem.price * SALES_TAX_RATE; 25 | 26 | return tally + cartItem.quantity; 27 | }, 0); 28 | } 29 | 30 | return { 31 | quantity, 32 | shipping: Number(parseFloat(shipping).toFixed(2)), 33 | tax: Number(parseFloat(tax).toFixed(2)), 34 | subtotal: Number(parseFloat(subtotal).toFixed(2)), 35 | sales_tax_rate: SALES_TAX_RATE, 36 | shipping_rate: SHIPPING_COST_PER_ITEM, 37 | }; 38 | }; 39 | 40 | module.exports = { 41 | hasPermission, 42 | getCartTotals, 43 | } 44 | -------------------------------------------------------------------------------- /client/lib/test-utils/mocks/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | mockUser, mockUsers, 3 | mockImage, mockImageVariables, 4 | mockProduct, mockProducts, mockShopProductsVariables, 5 | mockVariant, 6 | mockCartItem, 7 | mockOrderItem, mockOrder, 8 | } from './typeDefs'; 9 | export { 10 | localUserQueryEmptyCartMock, localUserQueryNoUserMock, localUserQueryCartItemMock, 11 | userQueryEmptyCartMock, userQueryNoUserMock, userQueryCartItemMock, 12 | signupMutationMock, 13 | signinMutationMock, 14 | signoutMutationMock, 15 | requestPasswordResetMutationMock, requestPasswordResetMutationErrorMock, 16 | resetPasswordMutationMock, resetPasswordMutationErrorMock, 17 | updatePermissionsMutationMock, 18 | createImageMutationMock, createImageMutationErrorMock, 19 | productQueryMock, productQueryErrorMock, productQueryNoProductMock, productQueryNoVariantMock, 20 | shopProductsQueryFilterMock, 21 | createProductMutationMock, createProductMutationErrorMock, 22 | updateProductMutationMock, updateProductMutationErrorMock, 23 | deleteProductMutationMock, 24 | createProductVariantMutationMock, createProductVariantMutationErrorMock, 25 | updateProductVariantMutationMock, updateProductVariantMutationErrorMock, 26 | deleteProductVariantMutationMock, 27 | addToCartMutationMock, 28 | updateCartItemMutationMock, 29 | removeFromCartMutationMock, 30 | } from './resolvers'; 31 | -------------------------------------------------------------------------------- /server/src/resolvers/Mutation/image.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const ImageMutations = { 4 | async createImage(parent, args, ctx, info) { 5 | const data = { ...args }; 6 | let productId; 7 | if (args.productId) { 8 | productId = args.productId; 9 | delete data.productId; 10 | } 11 | delete data.id; 12 | 13 | // Logged in? 14 | const { userId } = ctx.request; 15 | if (!userId) throw new Error('CREATE IMAGE: You must be signed in to create an image.'); 16 | 17 | // Existing image? 18 | const [existingImg] = await ctx.db.query.images({ 19 | where: { ...data } 20 | }, info); 21 | if (!!existingImg) return existingImg; 22 | 23 | const createdImage = await ctx.db.mutation.createImage({ 24 | data: { 25 | ...data, 26 | user: { connect: { id: userId } } 27 | } 28 | }, info); 29 | 30 | if (productId) { 31 | await ctx.db.mutation.updateProduct({ 32 | where: { id: productId }, 33 | data: { 34 | image: { connect: { id: createdImage.id }}, 35 | images: { connect: { id: createdImage.id }} 36 | } 37 | }); 38 | } 39 | 40 | return createdImage; 41 | }, 42 | async deleteImage(parent, args, ctx, info) { 43 | return await ctx.db.mutation.deleteImage({ 44 | where: { id: args.id } 45 | }, info); 46 | } 47 | }; 48 | 49 | 50 | module.exports = ImageMutations; 51 | -------------------------------------------------------------------------------- /client/pages/permissions.js: -------------------------------------------------------------------------------- 1 | import { Query } from 'react-apollo'; 2 | import { StyledCreatePage } from '../components/styles/PageStyles'; 3 | import PageTitle from '../components/PageTitle'; 4 | import RequireSignin from '../components/RequireSignin'; 5 | import NotFound from '../components/NotFound'; 6 | import Permissions from '../components/Permissions'; 7 | import { ALL_USERS_QUERY } from '../graphql'; 8 | 9 | 10 | const PermissionsPage = () => ( 11 | 12 | 13 | 14 | 15 | {({ me }) => ( 16 | 17 | {({ data, loading, error }) => { 18 | if (loading) return (

Loading...

); 19 | if (error) return ( 20 |
21 | 22 |
23 | ); 24 | const users = (!!data && data.users) 25 | ? data.users 26 | : []; 27 | return ( 28 |
29 | 30 |
31 | ); 32 | }} 33 |
34 | )} 35 |
36 |
37 | ); 38 | 39 | 40 | export default PermissionsPage; 41 | -------------------------------------------------------------------------------- /client/lib/test-utils/mocks/resolvers/cartItem.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART_MUTATION, 3 | UPDATE_CARTITEM_MUTATION, 4 | REMOVE_FROM_CART_MUTATION 5 | } from '../../../../graphql' 6 | import { mockCartItem, mockVariant } from '../typeDefs'; 7 | 8 | 9 | const addToCartMutationMock = { 10 | request: { query: ADD_TO_CART_MUTATION, variables: { id: mockVariant.id } }, 11 | result: { 12 | data: { 13 | addToCart: { 14 | __typename: mockCartItem.__typename, 15 | id: mockCartItem.id, 16 | }, 17 | }, 18 | }, 19 | }; 20 | 21 | const updateCartItemMutationMock = (quantity) => ({ 22 | request: { query: UPDATE_CARTITEM_MUTATION, variables: { id: mockCartItem.id, quantity } }, 23 | result: { 24 | data: { 25 | updateCartItem: { 26 | __typename: mockCartItem.__typename, 27 | id: mockCartItem.id, 28 | quantity, 29 | variant: { ...mockCartItem.variant } 30 | }, 31 | }, 32 | }, 33 | }); 34 | 35 | const removeFromCartMutationMock = { 36 | request: { query: REMOVE_FROM_CART_MUTATION, variables: { id: mockCartItem.id } }, 37 | result: { 38 | data: { 39 | removeFromCart: { 40 | __typename: mockCartItem.__typename, 41 | id: mockCartItem.id, 42 | }, 43 | }, 44 | }, 45 | }; 46 | 47 | 48 | export { 49 | addToCartMutationMock, 50 | updateCartItemMutationMock, 51 | removeFromCartMutationMock 52 | } 53 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "Next Store server-side", 5 | "main": "index.js", 6 | "author": "Alexandra Swart", 7 | "license": "ISC", 8 | "scripts": { 9 | "start:dev": "nodemon -e js,graphql -x node --inspect src/index.js", 10 | "start": "cross-env NODE_ENV=production nodemon -e js,graphql -x node src/index.js", 11 | "now-build": "rm -rf dist && npm run deploy:dev && npm run test", 12 | "deploy:dev": "prisma deploy --env-file .env", 13 | "deploy": "cross-env NODE_ENV=production prisma deploy --env-file .env", 14 | "test": "echo \"Error: no test specified\" && exit 0", 15 | "heroku-logs-server": "cd .. && heroku logs --tail --app next-store-yoga", 16 | "heroku-push-server": "cd .. && git subtree push --prefix server heroku-server-side master" 17 | }, 18 | "dependencies": { 19 | "babel-preset-env": "^1.7.0", 20 | "bcryptjs": "2.4.3", 21 | "cookie-parser": "^1.4.4", 22 | "cross-env": "^5.2.0", 23 | "dotenv": "^6.1.0", 24 | "graphql": "^0.13.2", 25 | "graphql-cli": "^3.0.14", 26 | "graphql-yoga": "1.16.7", 27 | "jsonwebtoken": "^8.5.1", 28 | "nodemailer": "^5.0.0", 29 | "nodemon": "1.18.5", 30 | "prisma": "^1.34.8", 31 | "prisma-binding": "^2.3.16" 32 | }, 33 | "devDependencies": {}, 34 | "babel": { 35 | "presets": [ 36 | "env" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import Link from 'next/link'; 3 | import styled from 'styled-components'; 4 | 5 | 6 | const StyledNotFound = styled.div` 7 | padding: 10rem 0; 8 | background: white; 9 | text-align: center; 10 | h1 { 11 | padding-bottom: 10rem; 12 | } 13 | p { 14 | margin: 0; 15 | padding: 2rem 0; 16 | font-weight: 100; 17 | } 18 | `; 19 | 20 | const NotFound = ({ status, message }) => { 21 | let title = 'Not Found'; 22 | 23 | if (status === 204) title = 'Nothing Here'; 24 | if (status === 401) title = 'Unauthorized'; 25 | if (status === 400) { 26 | title = 'Error'; 27 | message = 'An error occured. Please try again later.'; 28 | }; 29 | message = message 30 | .replace('GraphQL error:', '') 31 | .replace('Network error:', '') 32 | 33 | return ( 34 | 35 |

{title}

36 | 37 |

{message}

38 | 39 |

40 | Go back to 41 | 42 | home 43 | 44 | page. 45 |

46 |
47 | ) 48 | }; 49 | 50 | NotFound.defaultProps = { 51 | status: 404, 52 | message: 'Unable to find what you are looking for!' 53 | }; 54 | 55 | NotFound.propTypes = { 56 | status: PropTypes.number, 57 | message: PropTypes.string 58 | }; 59 | 60 | 61 | export default NotFound; 62 | -------------------------------------------------------------------------------- /client/__tests__/components/OrdersList.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import toJSON from 'enzyme-to-json'; 3 | import wait from 'waait'; 4 | import { MockedProvider } from 'react-apollo/test-utils'; 5 | import OrdersList from '../../components/OrdersList'; 6 | import { mockOrder } from '../../lib/test-utils/mocks'; 7 | 8 | 9 | describe('', () => { 10 | let wrapper; 11 | beforeAll(() => { 12 | wrapper = mount( 13 | 14 | 15 | 16 | ); 17 | }); 18 | afterAll(() => { 19 | wrapper.unmount(); 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('matches the snap shot', async () => { 24 | await wait(); 25 | wrapper.update(); 26 | expect(toJSON(wrapper.find('#orders-list'))).toMatchSnapshot(); 27 | }); 28 | 29 | it('renders properly', async () => { 30 | expect(wrapper.find(`#order-list-item-${mockOrder.id}`).length).toBe(1); 31 | expect(wrapper.find(`#order-list-item-${mockOrder.id}`).find('Link').props().href).toMatchObject({ 32 | pathname: "/order", 33 | query: { id: mockOrder.id } 34 | }); 35 | expect(wrapper.find('.order-list-item-details').length).toBe(1); 36 | expect(wrapper.find('.order-list-item-details').find('h3').find('span').length).toBe(2); 37 | expect(wrapper.find('.order-list-item-details').find('p').length).toBe(4); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /client/components/SvgIcon.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { svgs } from '../config'; 3 | 4 | 5 | const SvgIcon = ({ name, color, className, width, style, title }) => { 6 | const foundSvg = svgs[name]; 7 | const viewBox = (!!foundSvg && !!foundSvg.viewBox) 8 | ? foundSvg.viewBox 9 | : '0 0 32 32'; 10 | const ds = (!!foundSvg && !!foundSvg.ds) 11 | ? foundSvg.ds 12 | : []; 13 | let fill = '#6d6c6c'; 14 | 15 | switch(color) { 16 | case 'darkBlue': 17 | fill = '#47505f'; 18 | break; 19 | default: 20 | fill = '#6d6c6c'; 21 | break; 22 | } 23 | 24 | return ( 25 | 32 | 33 | {!!title && !!title.length && ( 34 | {title} 35 | )} 36 | {!!ds.length && ds.map((d, i) => ( 37 | 38 | ))} 39 | 40 | 41 | ) 42 | }; 43 | 44 | SvgIcon.propTypes = { 45 | name: PropTypes.string.isRequired, 46 | color: PropTypes.string, 47 | className: PropTypes.string, 48 | width: PropTypes.number, 49 | style: PropTypes.object, 50 | title: PropTypes.string, 51 | }; 52 | 53 | 54 | export default SvgIcon; 55 | -------------------------------------------------------------------------------- /client/__tests__/components/PageTitle.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import toJSON from 'enzyme-to-json'; 3 | import wait from 'waait'; 4 | import { MockedProvider } from 'react-apollo/test-utils'; 5 | import PageTitle from '../../components/PageTitle'; 6 | 7 | 8 | describe('', () => { 9 | let wrapper; 10 | beforeAll(() => { 11 | wrapper = mount( 12 | 13 | 35 | 36 | ); 37 | }); 38 | afterAll(() => wrapper.unmount()); 39 | 40 | it('matches the snap shot', async () => { 41 | await wait(); 42 | wrapper.update(); 43 | expect(toJSON(wrapper.find('PageTitle'))).toMatchSnapshot(); 44 | }); 45 | 46 | it('renders properly', async () => { 47 | expect(wrapper.text()).toContain('Some TitleSelectionsAdd Selection'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /client/__tests__/components/SalesList.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import toJSON from 'enzyme-to-json'; 3 | import wait from 'waait'; 4 | import { MockedProvider } from 'react-apollo/test-utils'; 5 | import SalesList from '../../components/SalesList'; 6 | import { mockOrderItem } from '../../lib/test-utils/mocks'; 7 | 8 | 9 | describe('', () => { 10 | let wrapper; 11 | beforeAll(() => { 12 | wrapper = mount( 13 | 14 | 15 | 16 | ); 17 | }); 18 | afterAll(() => { 19 | wrapper.unmount(); 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | // it('matches the snap shot', async () => { 24 | // await wait(); 25 | // wrapper.update(); 26 | // expect(toJSON(wrapper.find('#sales-list'))).toMatchSnapshot(); 27 | // }); 28 | 29 | it('renders properly', async () => { 30 | expect(wrapper.find(`#sales-list-item-${mockOrderItem.id}`).length).toBe(1); 31 | expect(wrapper.find(`#sales-list-item-${mockOrderItem.id}`).find('Link').props().href).toMatchObject({ 32 | pathname: "/product/edit", 33 | query: { id: mockOrderItem.variant.product.id } 34 | }); 35 | expect(wrapper.find('.order-list-item-details').length).toBe(1); 36 | expect(wrapper.find('.order-list-item-details').find('h3').find('span').length).toBe(2); 37 | expect(wrapper.find('.order-list-item-details').find('p').length).toBe(7); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /client/components/Buttons/UpdateCartItem.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Mutation } from 'react-apollo'; 3 | import { UPDATE_CARTITEM_MUTATION, CURRENT_USER_QUERY } from '../../graphql'; 4 | 5 | 6 | const UpdateCartItem = ({ id, quantity, disabled, children }) => { 7 | const update = (cache, payload) => { 8 | const data = cache.readQuery({ query: CURRENT_USER_QUERY }); 9 | const cartItemVariantId = payload.data.updateCartItem.variant.id; 10 | data.me.cart = data.me.cart.map(cartItem => { 11 | if (cartItem.variant.id === cartItemVariantId) cartItem.quantity = quantity; 12 | return cartItem; 13 | }); 14 | cache.writeQuery({ query: CURRENT_USER_QUERY, data }); 15 | }; 16 | return ( 17 | 21 | {(updateCartItem, { loading, error }) => ( 22 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | UpdateCartItem.propTypes = { 38 | id: PropTypes.string.isRequired, 39 | quantity: PropTypes.number.isRequired, 40 | disabled: PropTypes.bool.isRequired, 41 | }; 42 | 43 | 44 | export { UpdateCartItem }; 45 | -------------------------------------------------------------------------------- /client/graphql/Mutation/product.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const CREATE_PRODUCT_MUTATION = gql` 5 | mutation CREATE_PRODUCT_MUTATION( 6 | $title: String!, 7 | $department: String!, 8 | $description: String!, 9 | $category: String, 10 | $brand: String, 11 | $online: Boolean!, 12 | $imageId: String! 13 | ) { 14 | createProduct( 15 | title: $title, 16 | department: $department, 17 | description: $description, 18 | category: $category, 19 | brand: $brand, 20 | online: $online, 21 | imageId: $imageId 22 | ) { 23 | id 24 | } 25 | } 26 | `; 27 | 28 | const UPDATE_PRODUCT_MUTATION = gql` 29 | mutation UPDATE_PRODUCT_MUTATION( 30 | $id: ID!, 31 | $title: String!, 32 | $department: String!, 33 | $description: String!, 34 | $category: String, 35 | $brand: String, 36 | $online: Boolean!, 37 | $imageId: String! 38 | ) { 39 | updateProduct( 40 | id: $id, 41 | title: $title, 42 | department: $department, 43 | description: $description, 44 | category: $category, 45 | brand: $brand, 46 | online: $online, 47 | imageId: $imageId 48 | ) { 49 | id 50 | } 51 | } 52 | `; 53 | 54 | const DELETE_PRODUCT_MUTATION = gql` 55 | mutation DELETE_PRODUCT_MUTATION($id: ID!) { 56 | deleteProduct(id: $id) { 57 | id 58 | } 59 | } 60 | `; 61 | 62 | 63 | export { 64 | CREATE_PRODUCT_MUTATION, 65 | UPDATE_PRODUCT_MUTATION, 66 | DELETE_PRODUCT_MUTATION, 67 | } 68 | -------------------------------------------------------------------------------- /client/components/Buttons/RemoveFromCart.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Mutation } from 'react-apollo'; 3 | import { REMOVE_FROM_CART_MUTATION, CURRENT_USER_QUERY } from '../../graphql'; 4 | 5 | 6 | const RemoveFromCart = ({ id }) => { 7 | const update = (cache, payload) => { 8 | try { 9 | const data = cache.readQuery({ query: CURRENT_USER_QUERY }); 10 | const cartItemId = payload.data.removeFromCart.id; 11 | data.me.cart = data.me.cart.filter(cartItem => cartItem.id !== cartItemId); 12 | cache.writeQuery({ query: CURRENT_USER_QUERY, data }); 13 | } catch(e) {} 14 | }; 15 | return ( 16 | 27 | {(removeFromCart, { loading, error }) => ( 28 | 39 | )} 40 | 41 | ); 42 | }; 43 | 44 | RemoveFromCart.propTypes = { 45 | id: PropTypes.string.isRequired 46 | }; 47 | 48 | 49 | export { RemoveFromCart }; 50 | -------------------------------------------------------------------------------- /client/pages/buy.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import SingleProduct from '../components/SingleProduct'; 3 | import NotFound from '../components/NotFound'; 4 | import PageTitle from '../components/PageTitle'; 5 | import Product from '../components/Product'; 6 | import { AddToCart } from '../components/Buttons'; 7 | import { StyledBuyPage } from '../components/styles/PageStyles'; 8 | import { getPageTitleProps } from '../lib/utils'; 9 | 10 | 11 | const BuyPage = ({ query }) => ( 12 | 13 | {({ data, error, loading }) => { 14 | if (loading) return (

Loading...

); 15 | if (error) return (); 16 | const { product } = data; 17 | if (typeof product === 'undefined' || product === null) return (); 18 | const { titles } = getPageTitleProps(null, product); 19 | titles.push({ label: product.title }) 20 | return ( 21 | 22 | 26 | 27 |
28 | 33 |
34 |
35 | ) 36 | }} 37 |
38 | ); 39 | 40 | BuyPage.propTypes = { 41 | query: PropTypes.shape({ 42 | id: PropTypes.string 43 | }).isRequired 44 | }; 45 | 46 | 47 | export default BuyPage; 48 | -------------------------------------------------------------------------------- /client/__tests__/components/ProductsList.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import toJSON from 'enzyme-to-json'; 3 | import wait from 'waait'; 4 | import { MockedProvider } from 'react-apollo/test-utils'; 5 | import ProductsList from '../../components/ProductsList'; 6 | import { 7 | mockUser, mockProducts 8 | } from '../../lib/test-utils/mocks'; 9 | 10 | 11 | describe('', () => { 12 | let wrapper; 13 | const variantAction = jest.fn(); 14 | beforeAll(() => { 15 | wrapper = mount( 16 | 17 | 22 | 23 | ); 24 | }); 25 | afterAll(() => { 26 | wrapper.unmount(); 27 | jest.clearAllMocks(); 28 | }); 29 | 30 | it('matches the snap shot', async () => { 31 | await wait(); 32 | wrapper.update(); 33 | expect(toJSON(wrapper.find('ProductsList'))).toMatchSnapshot(); 34 | }); 35 | 36 | it('renders properly', async () => { 37 | expect(wrapper.find('PriceTag').length).toBe(1); 38 | expect(wrapper.find('PriceTag').text()).toBe('$35$30'); 39 | expect(wrapper.find('DeleteProduct').length).toBe(1); 40 | expect(wrapper.find('DeleteProduct').props().id).toBe('pr0duct1d'); 41 | expect(wrapper.find('DeleteProduct').props().userName).toBe('Miss Coleman Berge'); 42 | expect(wrapper.find('DeleteProduct').text()).toBe('Delete'); 43 | expect(wrapper.find('i.prdct-itm-actns').length).toBe(1); 44 | expect(wrapper.find('i.prdct-itm-actns').text()).toBe('(Offline)'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /client/pages/order.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Query } from 'react-apollo'; 3 | import PageTitle from '../components/PageTitle'; 4 | import NotFound from '../components/NotFound'; 5 | import RequireSignin from '../components/RequireSignin'; 6 | import Order from '../components/Order'; 7 | import { StyledOrderPage } from '../components/styles/PageStyles'; 8 | import { ORDER_QUERY } from '../graphql'; 9 | 10 | 11 | const OrderPage = props => ( 12 | 13 | 22 | 23 |
24 | 25 | {({ me }) => ( 26 | 27 | {({ data, error, loading }) => { 28 | if (loading) return (

Loading...

); 29 | if (error) return (); 30 | const { order } = data; 31 | if (!order) return (); 32 | return ( 33 | 34 | ); 35 | }} 36 |
37 | )} 38 |
39 |
40 |
41 | ); 42 | 43 | OrderPage.propTypes = { 44 | query: PropTypes.shape({ 45 | id: PropTypes.string 46 | }).isRequired 47 | }; 48 | 49 | 50 | export default OrderPage; 51 | -------------------------------------------------------------------------------- /client/components/Buttons/DeleteProductVariant.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Mutation } from 'react-apollo'; 4 | import { DELETE_PROD_VARIANT_MUTATION, PRODUCT_QUERY } from '../../graphql'; 5 | 6 | 7 | const DeleteProductVariant = props => { 8 | const { productId, id, children } = props; 9 | const update = (cache, payload) => { 10 | try { 11 | const variables = { id: productId }; 12 | const data = cache.readQuery({ query: PRODUCT_QUERY, variables }); 13 | data.product.variants = data.product.variants.filter(variant => variant.id !== payload.data.deleteProductVariant.id); 14 | cache.writeQuery({ query: PRODUCT_QUERY, variables, data }); 15 | } catch(e) {} 16 | }; 17 | return ( 18 | 22 | {(deleteProductVariant, { error }) => ( 23 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | DeleteProductVariant.propTypes = { 43 | productId: PropTypes.string.isRequired, 44 | id: PropTypes.string.isRequired, 45 | children: PropTypes.string.isRequired 46 | }; 47 | 48 | 49 | export { DeleteProductVariant }; 50 | -------------------------------------------------------------------------------- /client/lib/test-utils/mocks/typeDefs.js: -------------------------------------------------------------------------------- 1 | import { 2 | fakeUser, fakeImage, fakeProduct, fakeVariant, fakeCartItem, fakeOrder, fakeOrderItem, 3 | } from '../utils'; 4 | 5 | 6 | const mockUser = fakeUser(); 7 | const mockUser2 = fakeUser(); 8 | const mockImage = fakeImage(); 9 | const mockProduct = fakeProduct(); 10 | const mockVariant = fakeVariant(); 11 | const mockCartItem = fakeCartItem(); 12 | const mockOrder = fakeOrder(); 13 | const mockOrderItem = fakeOrderItem(); 14 | 15 | mockOrder.items.push(mockOrderItem); 16 | 17 | const mockUsers = [ 18 | mockUser, 19 | { 20 | ...mockUser2, 21 | id: '87654321', 22 | permissions: ['USER'], 23 | } 24 | ]; 25 | const mockProducts = [{ 26 | ...mockProduct, 27 | image: mockImage, 28 | user: { 29 | __typename: mockUser.__typename, 30 | id: mockUser.id, 31 | name: mockUser.name, 32 | }, 33 | variants: [{ 34 | ...mockVariant, 35 | product: { 36 | __typename: mockProduct.__typename, 37 | id: mockProduct.id, 38 | image: mockImage, 39 | } 40 | }] 41 | }]; 42 | 43 | const mockImageVariables = { 44 | cloudinary_id: mockImage.cloudinary_id, 45 | name: mockImage.name, 46 | height: mockImage.height, 47 | width: mockImage.width, 48 | transformation: mockImage.transformation, 49 | image_url: mockImage.image_url, 50 | large_image_url: mockImage.large_image_url 51 | }; 52 | 53 | const mockShopProductsVariables = { 54 | name: mockUser.name, 55 | orderBy: 'createdAt_DESC', 56 | skip: 0, 57 | first: 1 58 | }; 59 | 60 | 61 | export { 62 | mockUser, mockUsers, 63 | mockImage, mockImageVariables, 64 | mockProduct, mockProducts, mockShopProductsVariables, 65 | mockVariant, 66 | mockCartItem, 67 | mockOrder, mockOrderItem, 68 | }; 69 | -------------------------------------------------------------------------------- /client/graphql/Mutation/user.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | 4 | const SIGNUP_MUTATION = gql` 5 | mutation SIGNUP_MUTATION($email: String!, $name: String!, $password: String!) { 6 | createUser(email: $email, name: $name, password: $password) { 7 | id 8 | email 9 | name 10 | } 11 | } 12 | `; 13 | 14 | const SIGNIN_MUTATION = gql` 15 | mutation SIGNIN_MUTATION($email: String!, $password: String!) { 16 | signin(email: $email, password: $password) { 17 | id 18 | email 19 | name 20 | } 21 | } 22 | `; 23 | 24 | const SIGNOUT_MUTATION = gql` 25 | mutation SIGNOUT_MUTATION { 26 | signout { 27 | message 28 | } 29 | } 30 | `; 31 | 32 | const REQUEST_PASSWORD_RESET_MUTATION = gql` 33 | mutation REQUEST_PASSWORD_RESET_MUTATION($email: String!) { 34 | requestPasswordReset(email: $email) { 35 | message 36 | } 37 | } 38 | `; 39 | 40 | const RESET_PASSWORD_MUTATION = gql` 41 | mutation RESET_PASSWORD_MUTATION($resetToken: String!, $password: String!, $confirmPassword: String!) { 42 | resetPassword(resetToken: $resetToken, password: $password, confirmPassword: $confirmPassword) { 43 | id 44 | email 45 | name 46 | } 47 | } 48 | `; 49 | 50 | const UPDATE_PERMISSIONS_MUTATION = gql` 51 | mutation UPDATE_PERMISSIONS_MUTATION($permissions: [Permission], $userId: ID!) { 52 | updatePermissions(permissions: $permissions, userId: $userId) { 53 | id 54 | permissions 55 | name 56 | email 57 | } 58 | } 59 | `; 60 | 61 | 62 | export { 63 | SIGNUP_MUTATION, 64 | SIGNIN_MUTATION, 65 | SIGNOUT_MUTATION, 66 | REQUEST_PASSWORD_RESET_MUTATION, 67 | RESET_PASSWORD_MUTATION, 68 | UPDATE_PERMISSIONS_MUTATION, 69 | }; 70 | -------------------------------------------------------------------------------- /client/components/Filter/FilterRange.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { StyledFilterRange } from '../styles/FilterStyles'; 3 | 4 | 5 | const FilterRange = (props) => { 6 | const { name, currentFilter } = props; 7 | const [min, setMin] = useState(props.price_gte || '10'); 8 | const [max, setMax] = useState(props.price_lte || '350'); 9 | const handleChange = e => { 10 | if (!!e.preventDefault) e.preventDefault(); 11 | const { id, value } = e.currentTarget; 12 | if (id == 'price_gte' && min != value) setMin(value); 13 | if (id == 'price_lte' && max != value) setMax(value); 14 | } 15 | const handleMouseUp = e => { 16 | if (!!e.preventDefault) e.preventDefault(); 17 | const { id, value } = e.currentTarget; 18 | if ((id == 'price_gte' && props.price_gte != value) || (id == 'price_lte' && props.price_lte != value)) { 19 | props.updateFilter(e); 20 | } 21 | } 22 | return ( 23 | 24 |
25 | 35 | 45 |
46 |
47 | ${min} 48 | ${max} 49 |
50 |
51 | ) 52 | }; 53 | 54 | 55 | export default FilterRange; 56 | -------------------------------------------------------------------------------- /client/static/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #e86c52; 8 | position: fixed; 9 | z-index: 1031; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 5px; 14 | } 15 | 16 | /* Fancy blur effect */ 17 | #nprogress .peg { 18 | display: block; 19 | position: absolute; 20 | right: 0px; 21 | width: 100px; 22 | height: 100%; 23 | box-shadow: 0 0 10px #e86c52, 0 0 5px #e86c52; 24 | opacity: 1.0; 25 | -webkit-transform: rotate(3deg) translate(0px, -4px); 26 | -ms-transform: rotate(3deg) translate(0px, -4px); 27 | transform: rotate(3deg) translate(0px, -4px); 28 | } 29 | 30 | /* Remove these to get rid of the spinner */ 31 | #nprogress .spinner { 32 | display: block; 33 | position: fixed; 34 | z-index: 1031; 35 | top: 15px; 36 | right: 15px; 37 | } 38 | 39 | #nprogress .spinner-icon { 40 | width: 18px; 41 | height: 18px; 42 | box-sizing: border-box; 43 | border: solid 2px transparent; 44 | border-top-color: #e86c52; 45 | border-left-color: #e86c52; 46 | border-radius: 50%; 47 | -webkit-animation: nprogress-spinner 400ms linear infinite; 48 | animation: nprogress-spinner 400ms linear infinite; 49 | } 50 | 51 | .nprogress-custom-parent { 52 | overflow: hidden; 53 | position: relative; 54 | } 55 | 56 | .nprogress-custom-parent #nprogress .spinner, 57 | .nprogress-custom-parent #nprogress .bar { 58 | position: absolute; 59 | } 60 | 61 | @-webkit-keyframes nprogress-spinner { 62 | 0% { 63 | -webkit-transform: rotate(0deg); 64 | } 65 | 66 | 100% { 67 | -webkit-transform: rotate(360deg); 68 | } 69 | } 70 | 71 | @keyframes nprogress-spinner { 72 | 0% { 73 | transform: rotate(0deg); 74 | } 75 | 76 | 100% { 77 | transform: rotate(360deg); 78 | } 79 | } -------------------------------------------------------------------------------- /client/components/PageTitle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Link from 'next/link'; 4 | import Head from 'next/head'; 5 | import SvgIcon from './SvgIcon'; 6 | import { StyledPageTitle } from './styles/PageStyles'; 7 | 8 | 9 | const PageTitle = ({ page, titles }) => ( 10 | 11 | 12 | 13 | Next Store{!!page ? ` | ${page}` : ''} 14 | 15 | 16 | 17 | {!titles || !titles.length ? ( 18 | 19 | {page} 20 | 21 | ) : (titles.map((title, i) => 22 | 23 | {!!title.href && ( 24 | 25 | 26 | {title.label} 27 | 28 | 29 | )} 30 | 31 | {!!title.click && ( 32 | 35 | )} 36 | 37 | {!title.href && !title.click && ( 38 | 39 | {title.label} 40 | 41 | )} 42 | {(i !== (titles.length - 1)) && ( 43 | 44 | )} 45 | 46 | ))} 47 | 48 | ); 49 | 50 | PageTitle.defaultProps = { 51 | page: '', 52 | titles: [] 53 | }; 54 | 55 | PageTitle.propTypes = { 56 | page: PropTypes.string, 57 | titles: PropTypes.arrayOf(PropTypes.shape({ 58 | label: PropTypes.string.isRequired, 59 | href: PropTypes.shape({ 60 | pathname: PropTypes.string.isRequired, 61 | query: PropTypes.object, 62 | }), 63 | click: PropTypes.func 64 | })) 65 | }; 66 | 67 | 68 | export default PageTitle; 69 | -------------------------------------------------------------------------------- /client/components/Buttons/DeleteProduct.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Router from 'next/router'; 4 | import { Mutation } from 'react-apollo'; 5 | import { DELETE_PRODUCT_MUTATION, SHOP_PRODUCTS_QUERY } from '../../graphql'; 6 | 7 | 8 | const DeleteProduct = props => { 9 | const { id, userName, className, children } = props; 10 | const update = (cache, payload) => { 11 | try { 12 | const variables = { name: userName }; 13 | const data = cache.readQuery({ query: SHOP_PRODUCTS_QUERY, variables }); 14 | data.products = data.products.filter(product => product.id !== payload.data.deleteProduct.id); 15 | cache.writeQuery({ query: SHOP_PRODUCTS_QUERY, variables, data }); 16 | } catch(e) {} 17 | } 18 | return ( 19 | 23 | {(deleteProduct, { error }) => ( 24 | 41 | )} 42 | 43 | ) 44 | }; 45 | 46 | DeleteProduct.propTypes = { 47 | id: PropTypes.string.isRequired, 48 | userName: PropTypes.string.isRequired, 49 | className: PropTypes.string, 50 | children: PropTypes.string.isRequired 51 | }; 52 | 53 | 54 | export { DeleteProduct }; 55 | -------------------------------------------------------------------------------- /client/__tests__/components/Order.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import toJSON from 'enzyme-to-json'; 3 | import wait from 'waait'; 4 | import { MockedProvider } from 'react-apollo/test-utils'; 5 | import Order from '../../components/Order'; 6 | import { mockOrder } from '../../lib/test-utils/mocks'; 7 | 8 | 9 | describe('', () => { 10 | let wrapper; 11 | beforeAll(() => { 12 | wrapper = mount( 13 | 14 | 15 | 16 | ); 17 | }); 18 | afterAll(() => { 19 | wrapper.unmount(); 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('matches the snap shot', async () => { 24 | await wait(); 25 | wrapper.update(); 26 | expect(toJSON(wrapper.find('form[data-test="order"]'))).toMatchSnapshot(); 27 | }); 28 | 29 | it('renders properly', async () => { 30 | const header = wrapper.find('#order-header') 31 | expect(header.find('h2').length).toBe(1); 32 | expect(header.find('h2').find('span').length).toBe(2); 33 | expect(header.find('.order-payment-details').length).toBe(1); 34 | expect(header.find('.order-payment-details').find('p').length).toBe(3); 35 | const table = wrapper.find('table'); 36 | expect(table.length).toBe(1); 37 | expect(table.find('thead').length).toBe(1); 38 | expect(table.find('thead').find('th').length).toBe(3); 39 | expect(table.find('tbody').length).toBe(1); 40 | expect(table.find('tbody').find('tr').length).toBe(1); 41 | expect(table.find('tbody').find('td').length).toBe(3); 42 | expect(table.find('tbody').find('Link').props().href).toMatchObject({ 43 | pathname: "/buy", 44 | query: { id: mockOrder.items[0].variant.product.id } 45 | }); 46 | expect(table.find('tfoot').length).toBe(1); 47 | expect(table.find('tfoot').find('tr').length).toBe(4); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /client/lib/cloudinary.js: -------------------------------------------------------------------------------- 1 | import { CLOUDINARY_API_KEY, CLOUDINARY_PRESET, CLOUDINARY_SECRET } from '../config'; 2 | 3 | 4 | const uploadImageFile = async function(file) { 5 | const data = new FormData(); 6 | data.append('apiKey', CLOUDINARY_API_KEY); 7 | data.append('upload_preset', CLOUDINARY_PRESET); 8 | data.append('file', file); 9 | 10 | try { 11 | const res = await fetch('https://api.cloudinary.com/v1_1/answart/image/upload', { 12 | method: 'POST', 13 | body: data 14 | }).then(res => res.json()); 15 | 16 | if (!!res.error) throw new Error(res.error.message); 17 | if (!res.eager.length) throw new Error('Response gave no secure_url in res.eager list.'); 18 | 19 | return { 20 | cloudinary_id: res.public_id, 21 | name: res.original_filename, 22 | height: res.eager[0].height, 23 | width: res.eager[0].width, 24 | transformation: res.eager[0].transformation, 25 | image_url: res.secure_url, 26 | large_image_url: res.eager[0].secure_url, 27 | delete_token: res.delete_token 28 | } 29 | } catch(e) { 30 | console.error(`Error creating image file in cloudinary uploadImageFile. ${e}`); 31 | return { error: true, message: e }; 32 | } 33 | } 34 | 35 | const destroyImageFileByToken = async function(token) { 36 | const data = new FormData(); 37 | data.append('token', token); 38 | 39 | try { 40 | const res = await fetch('https://api.cloudinary.com/v1_1/answart/delete_by_token', { 41 | method: 'POST', 42 | body: data 43 | }).then(res => res.json()); 44 | 45 | if (!!res.error) throw res.error.message; 46 | return res; 47 | } catch(e) { 48 | console.error('Error in destroyImageFileByToken image file in cloudinary.', e); 49 | return { error: true, message: e }; 50 | } 51 | } 52 | 53 | 54 | export { 55 | uploadImageFile, 56 | destroyImageFileByToken 57 | }; 58 | -------------------------------------------------------------------------------- /client/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Link from 'next/link'; 3 | import Router from 'next/router'; 4 | import NProgress from 'nprogress'; 5 | import Search from './Search.js'; 6 | import Menu from './Menu.js'; 7 | import Nav from './Nav.js'; 8 | import Cart from '../Cart'; 9 | import StyledHeader from '../styles/HeaderStyles.js'; 10 | import { Query } from 'react-apollo'; 11 | import { CURRENT_USER_QUERY } from '../../graphql'; 12 | 13 | Router.onRouteChangeStart = () => { 14 | NProgress.start(); 15 | }; 16 | Router.onRouteChangeComplete = () => { 17 | NProgress.done(); 18 | }; 19 | Router.onRouteChangeError = () => { 20 | NProgress.done(); 21 | }; 22 | 23 | 24 | class Header extends Component { 25 | state = { acctDrpdwn: false }; 26 | toggAcctDrpdwn = e => { 27 | if (!!e && e.preventDefault) e.preventDefault(); 28 | 29 | this.setState(state => ({ acctDrpdwn: !state.acctDrpdwn })); 30 | }; 31 | render() { 32 | return ( 33 | 34 | {({ data, error }) => ( 35 | 36 |
37 | 38 | 39 |
40 | 41 | 42 | NextStore 43 | 44 | 45 |
46 | 47 | 52 |
53 | 54 | 57 | 58 |