├── .babelrc
├── .codeclimate.yml
├── .editorconfig
├── .env-sample
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .storybook
└── addons.js
├── README.md
├── components
├── App.js
├── Cart.js
├── CustomerAuth.js
├── ErrorMessage.js
├── Header.js
├── Layout.js
├── LineItem.js
├── PostList.js
├── PostUpvoter.js
├── Product.js
├── ProductList.js
├── Submit.js
├── ToggleCart.js
└── VariantSelector.js
├── containers
├── mutations
│ ├── checkoutMutations.js
│ └── withCheckoutCreate.js
├── queries
│ ├── withCheckout.js
│ ├── withProductList.js
│ ├── withReadCheckout.js
│ └── withShop.js
└── redux
│ ├── withCheckoutId.js
│ └── withIsCartOpen.js
├── fragments
└── checkoutFragment.js
├── lib
├── actions.js
├── initApollo.js
├── initRedux.js
├── reducers.js
└── withData.js
├── package-lock.json
├── package.json
├── pages
├── index.js
└── product.js
├── stories
├── data
│ ├── checkout.js
│ ├── lineItem.js
│ ├── option.js
│ └── product.js
├── index 10.44.39.js
├── index.js
└── storybook.css
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "next/babel"
4 | ],
5 | "plugins": [
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | duplication:
4 | enabled: true
5 | config:
6 | languages:
7 | - javascript
8 | eslint:
9 | enabled: true
10 | config:
11 | config: /.eslintrc
12 | checks:
13 | import/extensions:
14 | enabled: false
15 | prettier/prettier:
16 | enabled: false
17 | fixme:
18 | enabled: true
19 | ratings:
20 | paths:
21 | - "**.js"
22 | - "**.jsx"
23 | exclude_paths:
24 | - node_modules/
25 | - .next/
26 | - .gitattributes
27 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | indent_style = space
7 | indent_size = 2
8 |
--------------------------------------------------------------------------------
/.env-sample:
--------------------------------------------------------------------------------
1 | PORT=3000
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*{.,-}min.js
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "es6": true
7 | },
8 | "extends": [
9 | "airbnb",
10 | "prettier",
11 | "prettier/react"
12 | ],
13 | "plugins": [
14 | "prettier"
15 | ],
16 | "globals": {
17 | "document": true,
18 | "window": true,
19 | "process": true,
20 | "fetch": false,
21 | "ANALYTICS_TRACKING_ID": false,
22 | "AUTH0_CLIENT_ID": false,
23 | "AUTH0_DOMAIN": false,
24 | "GRAPHQL_ENDPOINT": false,
25 | "NEWSLETTER_FORM_ACTION": false,
26 | "NEWSLETTER_FORM_INPUT_NAME": false,
27 | "ON_PRODUCTION": true
28 | },
29 | "rules": {
30 | "react/forbid-prop-types": 0,
31 | "react/jsx-filename-extension": 0,
32 | "react/react-in-jsx-scope": 0,
33 | "class-methods-use-this": 0,
34 | "no-unused-expressions": ["error", { "allowTaggedTemplates": true }],
35 | "no-underscore-dangle": ["error", {
36 | "allow": ["__REDUX_DEVTOOLS_EXTENSION__"]
37 | }],
38 | "react/no-unused-prop-types": 0,
39 | "consistent-return": 0,
40 | "import/no-extraneous-dependencies": 0,
41 | "prettier/prettier": ["error", {
42 | "singleQuote": true
43 | }],
44 | "no-unused-vars": [1, {
45 | "vars": "all",
46 | "args": "none",
47 | "varsIgnorePattern": "React|PropTypes|Component"
48 | }],
49 | }
50 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .next/
3 | config.js
4 | .env
5 | coverage/
6 |
7 | # Created by https://www.gitignore.io/api/node,macos,linux,windows,webstorm,sublimetext,visualstudiocode
8 |
9 | ### Linux ###
10 | *~
11 |
12 | # temporary files which can be created if a process still has a handle open of a deleted file
13 | .fuse_hidden*
14 |
15 | # KDE directory preferences
16 | .directory
17 |
18 | # Linux trash folder which might appear on any partition or disk
19 | .Trash-*
20 |
21 | # .nfs files are created when an open file is removed but is still being accessed
22 | .nfs*
23 |
24 | ### macOS ###
25 | *.DS_Store
26 | .AppleDouble
27 | .LSOverride
28 |
29 | # Icon must end with two \r
30 | Icon
31 |
32 | # Thumbnails
33 | ._*
34 |
35 | # Files that might appear in the root of a volume
36 | .DocumentRevisions-V100
37 | .fseventsd
38 | .Spotlight-V100
39 | .TemporaryItems
40 | .Trashes
41 | .VolumeIcon.icns
42 | .com.apple.timemachine.donotpresent
43 |
44 | # Directories potentially created on remote AFP share
45 | .AppleDB
46 | .AppleDesktop
47 | Network Trash Folder
48 | Temporary Items
49 | .apdisk
50 |
51 | ### Node ###
52 | # Logs
53 | logs
54 | *.log
55 | npm-debug.log*
56 | yarn-debug.log*
57 | yarn-error.log*
58 |
59 | # Runtime data
60 | pids
61 | *.pid
62 | *.seed
63 | *.pid.lock
64 |
65 | # Directory for instrumented libs generated by jscoverage/JSCover
66 | lib-cov
67 |
68 | # Coverage directory used by tools like istanbul
69 | coverage
70 |
71 | # nyc test coverage
72 | .nyc_output
73 |
74 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
75 | .grunt
76 |
77 | # Bower dependency directory (https://bower.io/)
78 | bower_components
79 |
80 | # node-waf configuration
81 | .lock-wscript
82 |
83 | # Compiled binary addons (http://nodejs.org/api/addons.html)
84 | build/Release
85 |
86 | # Dependency directories
87 | node_modules/
88 | jspm_packages/
89 |
90 | # Typescript v1 declaration files
91 | typings/
92 |
93 | # Optional npm cache directory
94 | .npm
95 |
96 | # Optional eslint cache
97 | .eslintcache
98 |
99 | # Optional REPL history
100 | .node_repl_history
101 |
102 | # Output of 'npm pack'
103 | *.tgz
104 |
105 | # Yarn Integrity file
106 | .yarn-integrity
107 |
108 | # dotenv environment variables file
109 | .env
110 |
111 |
112 | ### SublimeText ###
113 | # cache files for sublime text
114 | *.tmlanguage.cache
115 | *.tmPreferences.cache
116 | *.stTheme.cache
117 |
118 | # workspace files are user-specific
119 | *.sublime-workspace
120 |
121 | # project files should be checked into the repository, unless a significant
122 | # proportion of contributors will probably not be using SublimeText
123 | # *.sublime-project
124 |
125 | # sftp configuration file
126 | sftp-config.json
127 |
128 | # Package control specific files
129 | Package Control.last-run
130 | Package Control.ca-list
131 | Package Control.ca-bundle
132 | Package Control.system-ca-bundle
133 | Package Control.cache/
134 | Package Control.ca-certs/
135 | Package Control.merged-ca-bundle
136 | Package Control.user-ca-bundle
137 | oscrypto-ca-bundle.crt
138 | bh_unicode_properties.cache
139 |
140 | # Sublime-github package stores a github token in this file
141 | # https://packagecontrol.io/packages/sublime-github
142 | GitHub.sublime-settings
143 |
144 | ### WebStorm ###
145 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
146 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
147 |
148 | # User-specific stuff:
149 | .idea/**/workspace.xml
150 | .idea/**/tasks.xml
151 | .idea/dictionaries
152 |
153 | # Sensitive or high-churn files:
154 | .idea/**/dataSources/
155 | .idea/**/dataSources.ids
156 | .idea/**/dataSources.xml
157 | .idea/**/dataSources.local.xml
158 | .idea/**/sqlDataSources.xml
159 | .idea/**/dynamic.xml
160 | .idea/**/uiDesigner.xml
161 |
162 | # Gradle:
163 | .idea/**/gradle.xml
164 | .idea/**/libraries
165 |
166 | # CMake
167 | cmake-build-debug/
168 |
169 | # Mongo Explorer plugin:
170 | .idea/**/mongoSettings.xml
171 |
172 | ## File-based project format:
173 | *.iws
174 |
175 | ## Plugin-specific files:
176 |
177 | # IntelliJ
178 | /out/
179 |
180 | # mpeltonen/sbt-idea plugin
181 | .idea_modules/
182 |
183 | # JIRA plugin
184 | atlassian-ide-plugin.xml
185 |
186 | # Cursive Clojure plugin
187 | .idea/replstate.xml
188 |
189 | # Crashlytics plugin (for Android Studio and IntelliJ)
190 | com_crashlytics_export_strings.xml
191 | crashlytics.properties
192 | crashlytics-build.properties
193 | fabric.properties
194 |
195 | ### WebStorm Patch ###
196 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
197 |
198 | # *.iml
199 | # modules.xml
200 | # .idea/misc.xml
201 | # *.ipr
202 |
203 | # Sonarlint plugin
204 | .idea/sonarlint
205 |
206 | ### Windows ###
207 | # Windows thumbnail cache files
208 | Thumbs.db
209 | ehthumbs.db
210 | ehthumbs_vista.db
211 |
212 | # Folder config file
213 | Desktop.ini
214 |
215 | # Recycle Bin used on file shares
216 | $RECYCLE.BIN/
217 |
218 | # Windows Installer files
219 | *.cab
220 | *.msi
221 | *.msm
222 | *.msp
223 |
224 | # Windows shortcuts
225 | *.lnk
226 |
227 | ### VisualStudioCode ###
228 | .vscode/*
229 | !.vscode/settings.json
230 | !.vscode/tasks.json
231 | !.vscode/launch.json
232 | !.vscode/extensions.json
233 |
234 | # End of https://www.gitignore.io/api/node,macos,linux,windows,webstorm,sublimetext,visualstudiocode
235 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */
2 |
3 | import '@storybook/addon-actions/register';
4 | import '@storybook/addon-links/register';
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | A React/GraphQL/Next.js boilerplate for Shopify
4 |
5 | ## Getting Started
6 |
7 | First, clone this repo. Then install it and run:
8 |
9 | ```bash
10 | npm install
11 | npm run dev
12 | ```
13 |
14 | Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
15 |
16 | ```bash
17 | now
18 | ```
19 |
20 | Also refer to the [Endpoint API documentation](https://help.shopify.com/api/storefront-api).
21 |
22 | ## Goals
23 |
24 | ### The Problem
25 |
26 | Shopify has one of the most user-friendly ecommerce back-ends out there, as well as an amazing range of plugins and extensions. But when it comes to developer workflow, it doesn't quite measure up to the modern JavaScript ecosystem.
27 |
28 | Some of the issues include:
29 |
30 | - Local theme development still requires a connection to preview your changes.
31 | - Deploying to staging requires manually switching to a different theme.
32 | - Using ES6/etc. requires running a build tool separately.
33 | - No support for more advanced front-end tools like React, Redux, etc.
34 | - No access to modern developer testing tools like [Storybook](http://storybook.js.org).
35 |
36 | ### The Solution
37 |
38 | This boilerplate project gives you a ready-made front-end for your Shopify store that will connect to your store's GraphQL endpoint. You can then host your front-end separately from your Shopify back-end, while benefiting from all the awesome JavaScript tools and libraries you're used to.
39 |
40 | ## Overview
41 |
42 | ### React
43 |
44 | [React](https://facebook.github.io/react/) has quickly established itself as a new standard for web and mobile development and its component-based approach is a perfect fit for complex ecommerce sites.
45 |
46 | ### GraphQL
47 |
48 | [GraphQL](http://graphql.org) is a new data querying syntax that lets you ask your back-end for what you need in a very granular and precise fashion.
49 |
50 | ### Apollo
51 |
52 | [Apollo Client](https://github.com/apollographql/apollo-client) is the GraphQL client used by this boilerplate. It will query your GraphQL endpoint, and then insert the results in the Redux store.
53 |
54 | ### Redux
55 |
56 | [Redux](http://redux.js.org/) helps manage your app's global state. It's used transparently by Apollo, but can also be used manually to keep track of things like UI state (cart shown/hidden, etc.).
57 |
58 | ### Next.js
59 |
60 | [Next.js](https://github.com/zeit/next.js) is used as a build tool and server, and takes care of server-side rendering. Alternatively, it can also export your site as a [static app](https://github.com/zeit/next.js#static-html-export), letting you host your front-end on static hosts like GitHub pages.
61 |
62 | ### Storybook
63 |
64 | [Storybook](http://storybook.js.org) is a component explorer for React. It lets you view all your components separately from your main app, which makes it much easier to keep track of them and test out their different states.
65 |
--------------------------------------------------------------------------------
/components/App.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Main App component
4 |
5 | - Create new checkout object
6 | - Add checkout ID to the Redux store
7 |
8 | */
9 | import React, { Component } from 'react';
10 |
11 | import { gql, graphql, compose } from 'react-apollo'
12 | import Layout from './Layout'
13 | import withShop from '../containers/queries/withShop'
14 | import withCheckoutId from '../containers/redux/withCheckoutId'
15 | import withCheckoutCreate from '../containers/mutations/withCheckoutCreate'
16 | import branch from 'recompose/branch'
17 | import { setCheckoutId } from '../lib/actions'
18 |
19 | class App extends Component {
20 |
21 | constructor() {
22 | super()
23 | }
24 |
25 | componentWillMount() {
26 | console.log('// App componentWillMount')
27 | this.props.checkoutCreate({
28 | variables: {
29 | input: {
30 | allowPartialAddresses: true,
31 | shippingAddress: {city: 'Toronto', province: 'ON', country: 'Canada'}
32 | }
33 | }
34 | }).then((res) => {
35 | console.log('// checkoutCreate mutation completed')
36 | // console.log(res.data.checkoutCreate)
37 | // store checkout ID in Redux (useful to retrieve checkout contents)
38 | const checkoutId = res.data.checkoutCreate.checkout.id
39 | this.props.dispatch(setCheckoutId(checkoutId))
40 | })
41 | }
42 |
43 | render() {
44 |
45 | const { loading, shop, children } = this.props
46 |
47 | return (
48 |
49 | {children}
50 |
522 |
523 | )
524 | }
525 | }
526 |
527 | export default compose(
528 | withShop,
529 | withCheckoutId,
530 | withCheckoutCreate
531 | )(App)
532 |
533 | // export default withShop(App)
--------------------------------------------------------------------------------
/components/Cart.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import LineItem from './LineItem';
3 | import withCheckoutId from '../containers/redux/withCheckoutId'
4 | import withIsCartOpen from '../containers/redux/withIsCartOpen'
5 |
6 | import withCheckout from '../containers/queries/withCheckout'
7 | import { setCheckoutId } from '../lib/actions'
8 | import { compose } from 'react-apollo'
9 | import branch from 'recompose/branch'
10 | import { openCart, closeCart } from '../lib/actions'
11 |
12 | class Cart extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 | this.openCheckout = this.openCheckout.bind(this)
17 | this.handleCartClose = this.handleCartClose.bind(this)
18 | }
19 |
20 | openCheckout() {
21 | window.open(this.props.checkout.webUrl);
22 | }
23 |
24 | handleCartClose() {
25 | this.props.dispatch(closeCart())
26 | }
27 |
28 |
29 | render() {
30 |
31 | if (!this.props.checkout) {
32 | return
Loading…
33 | }
34 |
35 | let line_items = this.props.checkout.lineItems.edges.map((line_item) => {
36 | return (
37 |
43 | );
44 | });
45 |
46 | return (
47 |
48 |
49 | Cart
50 |
55 |
56 |
59 |
80 |
81 | )
82 | }
83 | }
84 |
85 | Cart.defaultProps = {
86 | isCartOpen: true
87 | }
88 |
89 | export default compose(
90 | withIsCartOpen,
91 | withCheckoutId,
92 | branch( // only wrap with withCheckout if checkoutId is available
93 | props => !!props.checkoutId,
94 | withCheckout
95 | )
96 | )(Cart)
97 |
--------------------------------------------------------------------------------
/components/CustomerAuth.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { graphql, gql, compose } from 'react-apollo'
3 | import PropTypes from 'prop-types';
4 |
5 | class CustomerAuth extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | email: '',
10 | password: '',
11 | nonFieldErrorMessage: null,
12 | emailErrorMessage: null,
13 | passwordErrorMessage: null
14 | };
15 |
16 | this.handleInputChange = this.handleInputChange.bind(this);
17 | this.createCustomerAccount = this.createCustomerAccount.bind(this);
18 | this.resetErrorMessages = this.resetErrorMessages.bind(this);
19 | this.resetInputFields = this.resetInputFields.bind(this);
20 | }
21 |
22 | static propTypes = {
23 | customerCreate: PropTypes.func.isRequired,
24 | customerAccessTokenCreate: PropTypes.func.isRequired,
25 | }
26 |
27 | handleInputChange(event) {
28 | const target = event.target;
29 | const value = target.value;
30 | const name = target.name;
31 |
32 | this.setState({[name]: value});
33 | }
34 |
35 | resetErrorMessages(){
36 | this.setState({
37 | nonFieldErrorMessage: null,
38 | emailErrorMessage: null,
39 | passwordErrorMessage: null
40 | });
41 | }
42 |
43 | resetInputFields(){
44 | this.setState({
45 | email: '',
46 | password: ''
47 | });
48 | }
49 |
50 | handleSubmit(email, password){
51 | this.resetErrorMessages();
52 | if (this.props.newCustomer) {
53 | this.createCustomerAccount(email, password)
54 | } else {
55 | this.loginCustomerAccount(email, password)
56 | }
57 | }
58 |
59 | createCustomerAccount(email, password){
60 | const input = {
61 | email: email,
62 | password: password
63 | }
64 | this.props.customerCreate(
65 | { variables: { input }
66 | }).then((res) => {
67 | if (res.data.customerCreate.customer){
68 | this.props.closeCustomerAuth();
69 | this.props.showAccountVerificationMessage();
70 | } else {
71 | res.data.customerCreate.userErrors.forEach(function (error) {
72 | if (error.field) {
73 | this.setState({
74 | [error.field + "ErrorMessage"]: error.message
75 | });
76 | } else {
77 | this.setState({
78 | nonFieldErrorMessage: error.message
79 | });
80 | }
81 | }.bind(this));
82 | }
83 | });
84 | }
85 |
86 | loginCustomerAccount(email, password){
87 | const input = {
88 | email: email,
89 | password: password
90 | }
91 | this.props.customerAccessTokenCreate(
92 | { variables: { input }
93 | }).then((res) => {
94 | if (res.data.customerAccessTokenCreate.customerAccessToken) {
95 | this.props.associateCustomerCheckout(res.data.customerAccessTokenCreate.customerAccessToken.accessToken);
96 | } else {
97 | res.data.customerAccessTokenCreate.userErrors.forEach(function (error) {
98 | if (error.field != null) {
99 | this.setState({
100 | [error.field + "ErrorMessage"]: error.message
101 | });
102 | } else {
103 | this.setState({
104 | nonFieldErrorMessage: error.message
105 | });
106 | }
107 | }.bind(this));
108 | }
109 | });
110 | }
111 |
112 | render() {
113 | return (
114 |
115 |
120 |
121 |
{this.props.newCustomer ? 'Create your Account' : 'Log in to your account'}
122 | {this.state.nonFieldErrorMessage &&
123 |
{this.state.nonFieldErrorMessage}
124 | }
125 |
131 |
137 |
138 |
139 |
140 |
141 | )
142 | }
143 | }
144 |
145 | const customerCreate = gql`
146 | mutation customerCreate($input: CustomerCreateInput!) {
147 | customerCreate(input: $input) {
148 | userErrors {
149 | field
150 | message
151 | }
152 | customer {
153 | id
154 | }
155 | }
156 | }
157 | `;
158 |
159 | const customerAccessTokenCreate = gql`
160 | mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
161 | customerAccessTokenCreate(input: $input) {
162 | userErrors {
163 | field
164 | message
165 | }
166 | customerAccessToken {
167 | accessToken
168 | expiresAt
169 | }
170 | }
171 | }
172 | `;
173 |
174 | const CustomerAuthWithMutation = compose(
175 | graphql(customerCreate, {name: "customerCreate"}),
176 | graphql(customerAccessTokenCreate, {name: "customerAccessTokenCreate"})
177 | )(CustomerAuth);
178 |
179 | export default CustomerAuthWithMutation;
180 |
--------------------------------------------------------------------------------
/components/ErrorMessage.js:
--------------------------------------------------------------------------------
1 | export default ({message}) => (
2 |
13 | )
14 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export default ({ pathname }) => (
4 |
5 |
6 | Home
7 |
8 |
9 |
10 | About
11 |
12 |
13 |
26 |
27 | )
28 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import Cart from './Cart'
3 |
4 | import ToggleCart from './ToggleCart'
5 |
6 | const Layout = ({ children, shop, loading }) =>
7 |
8 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | export default Layout
--------------------------------------------------------------------------------
/components/LineItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class LineItem extends Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.decrementQuantity = this.decrementQuantity.bind(this);
8 | this.incrementQuantity = this.incrementQuantity.bind(this);
9 | }
10 |
11 | decrementQuantity(lineItemId) {
12 | this.props.updateLineItemInCart(lineItemId, this.props.line_item.quantity - 1)
13 | }
14 |
15 | incrementQuantity(lineItemId) {
16 | this.props.updateLineItemInCart(lineItemId, this.props.line_item.quantity + 1)
17 | }
18 |
19 | render() {
20 | return (
21 |
22 |
23 | {this.props.line_item.variant.image ?

: null}
24 |
25 |
26 |
27 |
28 | {this.props.line_item.variant.title}
29 |
30 |
31 | {this.props.line_item.title}
32 |
33 |
34 |
35 |
36 |
37 | {this.props.line_item.quantity}
38 |
39 |
40 |
41 | $ { (this.props.line_item.quantity * this.props.line_item.variant.price).toFixed(2) }
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
51 | export default LineItem;
52 |
--------------------------------------------------------------------------------
/components/PostList.js:
--------------------------------------------------------------------------------
1 | import { gql, graphql } from 'react-apollo'
2 | import ErrorMessage from './ErrorMessage'
3 | import PostUpvoter from './PostUpvoter'
4 |
5 | const POSTS_PER_PAGE = 10
6 |
7 | function PostList ({ data: { loading, error, allPosts, _allPostsMeta }, loadMorePosts }) {
8 | if (error) return
9 | if (allPosts && allPosts.length) {
10 | const areMorePosts = allPosts.length < _allPostsMeta.count
11 | return (
12 |
13 |
14 | {allPosts.map((post, index) =>
15 | -
16 |
21 |
22 | )}
23 |
24 | {areMorePosts ? : ''}
25 |
63 |
64 | )
65 | }
66 | return Loading
67 | }
68 |
69 | const allPosts = gql`
70 | query allPosts($first: Int!, $skip: Int!) {
71 | allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
72 | id
73 | title
74 | votes
75 | url
76 | createdAt
77 | },
78 | _allPostsMeta {
79 | count
80 | }
81 | }
82 | `
83 |
84 | // The `graphql` wrapper executes a GraphQL query and makes the results
85 | // available on the `data` prop of the wrapped component (PostList)
86 | export default graphql(allPosts, {
87 | options: {
88 | variables: {
89 | skip: 0,
90 | first: POSTS_PER_PAGE
91 | }
92 | },
93 | props: ({ data }) => ({
94 | data,
95 | loadMorePosts: () => {
96 | return data.fetchMore({
97 | variables: {
98 | skip: data.allPosts.length
99 | },
100 | updateQuery: (previousResult, { fetchMoreResult }) => {
101 | if (!fetchMoreResult) {
102 | return previousResult
103 | }
104 | return Object.assign({}, previousResult, {
105 | // Append the new posts results to the old one
106 | allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
107 | })
108 | }
109 | })
110 | }
111 | })
112 | })(PostList)
113 |
114 |
--------------------------------------------------------------------------------
/components/PostUpvoter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { gql, graphql } from 'react-apollo'
3 |
4 | function PostUpvoter ({ upvote, votes, id }) {
5 | return (
6 |
29 | )
30 | }
31 |
32 | const upvotePost = gql`
33 | mutation updatePost($id: ID!, $votes: Int) {
34 | updatePost(id: $id, votes: $votes) {
35 | id
36 | __typename
37 | votes
38 | }
39 | }
40 | `
41 |
42 | export default graphql(upvotePost, {
43 | props: ({ ownProps, mutate }) => ({
44 | upvote: (id, votes) => mutate({
45 | variables: { id, votes },
46 | optimisticResponse: {
47 | __typename: 'Mutation',
48 | updatePost: {
49 | __typename: 'Post',
50 | id: ownProps.id,
51 | votes: ownProps.votes + 1
52 | }
53 | }
54 | })
55 | })
56 | })(PostUpvoter)
57 |
--------------------------------------------------------------------------------
/components/Product.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import VariantSelector from './VariantSelector';
3 | // import withCheckoutAdd from '../containers/mutations/withCheckoutAdd'
4 | import { withCheckoutLineItemsAdd } from '../containers/mutations/checkoutMutations'
5 | import { connect } from 'react-redux';
6 | import withIsCartOpen from '../containers/redux/withIsCartOpen'
7 | import { openCart } from '../lib/actions'
8 |
9 | export class Product extends Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {};
14 |
15 | this.handleOptionChange = this.handleOptionChange.bind(this);
16 | this.handleQuantityChange = this.handleQuantityChange.bind(this);
17 | this.findImage = this.findImage.bind(this);
18 | this.addVariantToCart = this.addVariantToCart.bind(this);
19 | }
20 |
21 | componentWillMount() {
22 | this.props.product.options.forEach((selector) => {
23 | this.setState({
24 | selectedOptions: { [selector.name]: selector.values[0] }
25 | });
26 | });
27 | }
28 |
29 | findImage(images, variantId) {
30 | const primary = images[0];
31 |
32 | const image = images.filter(function (image) {
33 | return image.variant_ids.includes(variantId);
34 | })[0];
35 |
36 | return (image || primary).src;
37 | }
38 |
39 | handleOptionChange(event) {
40 | const target = event.target
41 | let selectedOptions = this.state.selectedOptions;
42 | selectedOptions[target.name] = target.value;
43 |
44 | const selectedVariant = this.props.product.variants.edges.find((variant) => {
45 | return variant.node.selectedOptions.every((selectedOption) => {
46 | return selectedOptions[selectedOption.name] === selectedOption.value;
47 | });
48 | }).node;
49 |
50 | this.setState({
51 | selectedVariant: selectedVariant,
52 | selectedVariantImage: selectedVariant.image.src
53 | });
54 | }
55 |
56 | handleQuantityChange(event) {
57 | this.setState({
58 | selectedVariantQuantity: event.target.value
59 | });
60 | }
61 |
62 | addVariantToCart(event) {
63 | event.preventDefault()
64 |
65 | const variant = this.state.selectedVariant || this.props.product.variants.edges[0].node
66 | const variantId = variant.id
67 | const quantity = this.state.selectedVariantQuantity || 1
68 |
69 | this.props.checkoutLineItemsAdd({
70 | variables: {
71 | checkoutId: this.props.checkoutId,
72 | lineItems: [
73 | {variantId, quantity: parseInt(quantity, 10)}
74 | ]
75 | }
76 | })
77 |
78 | this.props.dispatch(openCart())
79 |
80 | }
81 |
82 | render() {
83 | let variantImage = this.state.selectedVariantImage || this.props.product.images.edges[0].node.src
84 | let variant = this.state.selectedVariant || this.props.product.variants.edges[0].node
85 | let variantQuantity = this.state.selectedVariantQuantity || 1
86 | let variant_selectors = this.props.product.options.map((option) => {
87 | return (
88 |
93 | );
94 | });
95 | return (
96 |
97 | {this.props.product.images.edges.length ?

: null}
98 |
{this.props.product.title}
99 |
${variant.price}
100 | {variant_selectors}
101 |
105 |
106 |
107 | );
108 | }
109 | }
110 |
111 | export default withIsCartOpen(withCheckoutLineItemsAdd(Product));
112 |
--------------------------------------------------------------------------------
/components/ProductList.js:
--------------------------------------------------------------------------------
1 | import ErrorMessage from './ErrorMessage'
2 | import PostUpvoter from './PostUpvoter'
3 | import Product from './Product'
4 | import withProductList from '../containers/queries/withProductList'
5 | import withCheckoutId from '../containers/redux/withCheckoutId'
6 |
7 | const ProductList = ({checkoutId, products, loading}) => {
8 | if (loading) {
9 | return Loading…
10 | }
11 | return (
12 |
13 | {products.map(product =>
14 |
15 | )}
16 |
17 | )
18 | }
19 |
20 | export default withCheckoutId(withProductList(ProductList))
--------------------------------------------------------------------------------
/components/Submit.js:
--------------------------------------------------------------------------------
1 | import { gql, graphql } from 'react-apollo'
2 |
3 | function Submit ({ createPost }) {
4 | function handleSubmit (e) {
5 | e.preventDefault()
6 |
7 | let title = e.target.elements.title.value
8 | let url = e.target.elements.url.value
9 |
10 | if (title === '' || url === '') {
11 | window.alert('Both fields are required.')
12 | return false
13 | }
14 |
15 | // prepend http if missing from url
16 | if (!url.match(/^[a-zA-Z]+:\/\//)) {
17 | url = `http://${url}`
18 | }
19 |
20 | createPost(title, url)
21 |
22 | // reset form
23 | e.target.elements.title.value = ''
24 | e.target.elements.url.value = ''
25 | }
26 |
27 | return (
28 |
48 | )
49 | }
50 |
51 | const createPost = gql`
52 | mutation createPost($title: String!, $url: String!) {
53 | createPost(title: $title, url: $url) {
54 | id
55 | title
56 | votes
57 | url
58 | createdAt
59 | }
60 | }
61 | `
62 |
63 | export default graphql(createPost, {
64 | props: ({ mutate }) => ({
65 | createPost: (title, url) => mutate({
66 | variables: { title, url },
67 | updateQueries: {
68 | allPosts: (previousResult, { mutationResult }) => {
69 | const newPost = mutationResult.data.createPost
70 | return Object.assign({}, previousResult, {
71 | // Append the new post
72 | allPosts: [newPost, ...previousResult.allPosts]
73 | })
74 | }
75 | }
76 | })
77 | })
78 | })(Submit)
79 |
--------------------------------------------------------------------------------
/components/ToggleCart.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import withIsCartOpen from '../containers/redux/withIsCartOpen'
3 | import { toggleCart } from '../lib/actions'
4 |
5 | const ToggleCart = ({ dispatch }) =>
6 |
12 |
13 | export default withIsCartOpen(ToggleCart)
--------------------------------------------------------------------------------
/components/VariantSelector.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class VariantSelector extends Component {
4 | render() {
5 | return (
6 |
18 | );
19 | }
20 | }
21 |
22 | export default VariantSelector;
23 |
--------------------------------------------------------------------------------
/containers/mutations/checkoutMutations.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Define HoCs to add, update, remove line items, and associate checkout with customer
4 | */
5 | import React from 'react'
6 | import { graphql, gql, compose, withApollo } from 'react-apollo'
7 | import CheckoutFragment from '../../fragments/checkoutFragment'
8 |
9 | /*
10 |
11 | Add a Line Item
12 |
13 | */
14 | export const checkoutLineItemsAdd = gql`
15 | mutation checkoutLineItemsAdd ($checkoutId: ID!, $lineItems: [CheckoutLineItemInput!]!) {
16 | checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) {
17 | userErrors {
18 | message
19 | field
20 | }
21 | checkout {
22 | ...CheckoutFragment
23 | }
24 | }
25 | }
26 | ${CheckoutFragment}
27 | `;
28 |
29 | export const withCheckoutLineItemsAdd = graphql(checkoutLineItemsAdd, {name: 'checkoutLineItemsAdd', alias: 'withCheckoutLineItemsAdd'})
30 |
31 | /*
32 |
33 | Update a Line Item
34 |
35 | */
36 | export const checkoutLineItemsUpdate = gql`
37 | mutation checkoutLineItemsUpdate ($checkoutId: ID!, $lineItems: [CheckoutLineItemUpdateInput!]!) {
38 | checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) {
39 | userErrors {
40 | message
41 | field
42 | }
43 | checkout {
44 | ...CheckoutFragment
45 | }
46 | }
47 | }
48 | ${CheckoutFragment}
49 | `;
50 |
51 | export const withCheckoutLineItemsUpdate = graphql(checkoutLineItemsUpdate, {name: 'checkoutLineItemsUpdate', alias: 'withCheckoutLineItemsUpdate'})
52 |
53 | /*
54 |
55 | Remove a Line Item
56 |
57 | */
58 | export const checkoutLineItemsRemove = gql`
59 | mutation checkoutLineItemsRemove ($checkoutId: ID!, $lineItemIds: [ID!]!) {
60 | checkoutLineItemsRemove(checkoutId: $checkoutId, lineItemIds: $lineItemIds) {
61 | userErrors {
62 | message
63 | field
64 | }
65 | checkout {
66 | ...CheckoutFragment
67 | }
68 | }
69 | }
70 | ${CheckoutFragment}
71 | `;
72 |
73 | export const withCheckoutLineItemsRemove = graphql(checkoutLineItemsRemove, {name: 'checkoutLineItemsRemove', alias: 'withCheckoutLineItemsRemove'})
74 |
75 | /*
76 |
77 | Associate a Checkout with a Customer
78 |
79 | */
80 | export const checkoutCustomerAssociate = gql`
81 | mutation checkoutCustomerAssociate($checkoutId: ID!, $customerAccessToken: String!) {
82 | checkoutCustomerAssociate(checkoutId: $checkoutId, customerAccessToken: $customerAccessToken) {
83 | userErrors {
84 | field
85 | message
86 | }
87 | checkout {
88 | ...CheckoutFragment
89 | }
90 | }
91 | }
92 | ${CheckoutFragment}
93 | `;
94 |
95 | export const withCheckoutCustomerAssociate = graphql(checkoutCustomerAssociate, {name: 'checkoutCustomerAssociate', alias: 'withCheckoutCustomerAssociate'})
96 |
--------------------------------------------------------------------------------
/containers/mutations/withCheckoutCreate.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Mutation query HoC that creates a new checkout object
4 |
5 | */
6 | import React from 'react'
7 | import { graphql, gql } from 'react-apollo'
8 | import CheckoutFragment from '../../fragments/checkoutFragment'
9 |
10 | export const checkoutCreate = gql`
11 | mutation checkoutCreate ($input: CheckoutCreateInput!){
12 | checkoutCreate(input: $input) {
13 | userErrors {
14 | message
15 | field
16 | }
17 | checkout {
18 | ...CheckoutFragment
19 | }
20 | }
21 | }
22 | ${CheckoutFragment}
23 | `
24 |
25 | export default graphql(checkoutCreate, {name: 'checkoutCreate', alias: 'withCheckoutCreate'})
--------------------------------------------------------------------------------
/containers/queries/withCheckout.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Query to get Checkout object
4 |
5 | */
6 | import { gql, graphql } from 'react-apollo'
7 | import CheckoutFragment from '../../fragments/checkoutFragment'
8 |
9 | const checkout = gql`
10 | query checkoutQuery($id: ID!) {
11 | node(id:$id) {
12 | id
13 | ... on Checkout{
14 | ...CheckoutFragment
15 | }
16 | }
17 | }
18 | ${CheckoutFragment}
19 | `
20 |
21 | export default graphql(checkout, {
22 |
23 | alias: 'withCheckout',
24 |
25 | options(props) {
26 | return {
27 | variables: {
28 | // id: 'Z2lkOi8vc2hvcGlmeS9DaGVja291dC9hN2FhMTQwMDM0Njg0NDJmNDQ4ZmE1NmRjZmEyNjRiYT9rZXk9ZmJkYzc5ZmYwZGJiMTEwZmExNWI4MDE5ZmRlNzVlZDQ='
29 | id: props.checkoutId
30 | }
31 | }
32 | },
33 |
34 | props: ({ data }) => {
35 | return {checkout: data.node}
36 | }
37 | })
38 |
39 |
--------------------------------------------------------------------------------
/containers/queries/withProductList.js:
--------------------------------------------------------------------------------
1 | import { gql, graphql } from 'react-apollo'
2 |
3 | const POSTS_PER_PAGE = 10
4 |
5 | const productList = gql`
6 | query productListQuery {
7 | shop {
8 | products(first:20) {
9 | pageInfo {
10 | hasNextPage
11 | hasPreviousPage
12 | }
13 | edges {
14 | node {
15 | id
16 | title
17 | options {
18 | id
19 | name
20 | values
21 | }
22 | variants(first: 250) {
23 | pageInfo {
24 | hasNextPage
25 | hasPreviousPage
26 | }
27 | edges {
28 | node {
29 | id
30 | title
31 | selectedOptions {
32 | name
33 | value
34 | }
35 | image {
36 | src
37 | }
38 | price
39 | }
40 | }
41 | }
42 | images(first: 250) {
43 | pageInfo {
44 | hasNextPage
45 | hasPreviousPage
46 | }
47 | edges {
48 | node {
49 | src
50 | }
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 | `
59 |
60 | export default graphql(productList, {
61 | // options: {
62 | // variables: {
63 | // skip: 0,
64 | // first: POSTS_PER_PAGE
65 | // }
66 | // },
67 |
68 | alias: 'withProductList',
69 |
70 | props: ({ data }) => {
71 |
72 | return {
73 | products: data.shop && data.shop.products && data.shop.products.edges,
74 | loading: data.loading
75 | };
76 |
77 | // loadMorePosts: () => {
78 | // return data.fetchMore({
79 | // variables: {
80 | // skip: data.allPosts.length
81 | // },
82 | // updateQuery: (previousResult, { fetchMoreResult }) => {
83 | // if (!fetchMoreResult) {
84 | // return previousResult
85 | // }
86 | // return Object.assign({}, previousResult, {
87 | // // Append the new posts results to the old one
88 | // products: [...previousResult.products, ...fetchMoreResult.products]
89 | // })
90 | // }
91 | // })
92 | // }
93 | }
94 | })
--------------------------------------------------------------------------------
/containers/queries/withReadCheckout.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | HoC that reads an existing Checkout object from the store based on its id
4 |
5 | NOT USED
6 |
7 | */
8 | import React from 'react'
9 | import { graphql, gql, compose, withApollo } from 'react-apollo'
10 | import CheckoutFragment from '../../fragments/checkoutFragment'
11 | import withCheckoutId from './withCheckoutId'
12 |
13 | export const readCheckout = WrappedComponent => {
14 | return class extends React.Component{
15 | render() {
16 | if (this.props.checkoutId) {
17 | const checkout = this.props.client.readFragment({
18 | id: this.props.checkoutId, // `id` is any id that could be returned by `dataIdFromObject`.
19 | fragment: CheckoutFragment
20 | })
21 | return
22 | } else {
23 | return
24 | }
25 | }
26 | }
27 | }
28 |
29 | export default compose(
30 | withCheckoutId,
31 | readCheckout,
32 | )
--------------------------------------------------------------------------------
/containers/queries/withShop.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Get main shop info
4 |
5 | */
6 | import { gql, graphql } from 'react-apollo'
7 |
8 | const shopInfo = gql`
9 | query shopInfoQuery {
10 | shop {
11 | name
12 | description
13 | }
14 | }
15 | `
16 |
17 | export default graphql(shopInfo, {
18 |
19 | alias: 'withShopInfo',
20 |
21 | props: ({ data }) => {
22 | return {
23 | shop: data.shop,
24 | loading: data.loading
25 | };
26 |
27 | }
28 | })
--------------------------------------------------------------------------------
/containers/redux/withCheckoutId.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Get checkout ID from Redux store.
4 |
5 | Also pass down `dispatch` method used to *set* checkout ID
6 |
7 | */
8 | import { connect } from 'react-redux';
9 |
10 | const withCheckoutId = connect(
11 | (state) => ({ checkoutId: state.checkoutId }),
12 | )
13 |
14 | export default withCheckoutId
--------------------------------------------------------------------------------
/containers/redux/withIsCartOpen.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Get cart state from Redux
4 |
5 | Also pass down `dispatch` method used to *set* checkout ID
6 |
7 | */
8 | import { connect } from 'react-redux';
9 |
10 | const withIsCartOpen = connect(
11 | (state) => ({ isCartOpen: state.isCartOpen }),
12 | )
13 |
14 | export default withIsCartOpen
--------------------------------------------------------------------------------
/fragments/checkoutFragment.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'react-apollo'
2 |
3 | const CheckoutFragment = gql`
4 | fragment CheckoutFragment on Checkout {
5 | id
6 | webUrl
7 | totalTax
8 | subtotalPrice
9 | totalPrice
10 | lineItems (first: 250) {
11 | edges {
12 | node {
13 | id
14 | title
15 | variant {
16 | id
17 | title
18 | image {
19 | src
20 | }
21 | price
22 | }
23 | quantity
24 | }
25 | }
26 | }
27 | }
28 | `;
29 |
30 | export default CheckoutFragment
--------------------------------------------------------------------------------
/lib/actions.js:
--------------------------------------------------------------------------------
1 | export const setCheckoutId = (id) => ({
2 | type: 'SET_CHECKOUT_ID',
3 | payload: { id }
4 | })
5 |
6 | export const toggleCart = () => ({
7 | type: 'TOGGLE_CART',
8 | payload: {}
9 | })
10 |
11 | export const openCart = () => ({
12 | type: 'OPEN_CART',
13 | payload: {}
14 | })
15 |
16 | export const closeCart = () => ({
17 | type: 'CLOSE_CART',
18 | payload: {}
19 | })
--------------------------------------------------------------------------------
/lib/initApollo.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, createNetworkInterface } from 'react-apollo'
2 | import { toIdValue } from 'apollo-client'
3 | import fetch from 'isomorphic-fetch'
4 |
5 | let apolloClient = null
6 |
7 | // Polyfill fetch() on the server (used by apollo-client)
8 | if (!process.browser) {
9 | global.fetch = fetch
10 | }
11 |
12 | const dataIdFromObject = object => object.id
13 |
14 | function create () {
15 | return new ApolloClient({
16 | dataIdFromObject,
17 | ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
18 | networkInterface: createNetworkInterface({
19 | uri: 'https://graphql.myshopify.com/api/graphql', // Server URL (must be absolute)
20 | opts: { // Additional fetch() options like `credentials` or `headers`
21 | credentials: 'same-origin',
22 | headers: {
23 | 'X-Shopify-Storefront-Access-Token': 'dd4d4dc146542ba7763305d71d1b3d38'
24 | }
25 | }
26 | }),
27 | // customResolvers: {
28 | // Query: {
29 | // node: (_, args) => {
30 | // console.log('// customResolvers')
31 | // console.log(_)
32 | // console.log(args)
33 | // return toIdValue(dataIdFromObject({ __typename: 'Checkout', id: args.id }))
34 | // },
35 | // },
36 | // },
37 | })
38 | }
39 |
40 | export default function initApollo () {
41 | // Make sure to create a new client for every server-side request so that data
42 | // isn't shared between connections (which would be bad)
43 | if (!process.browser) {
44 | return create()
45 | }
46 |
47 | // Reuse client on the client-side
48 | if (!apolloClient) {
49 | apolloClient = create()
50 | }
51 |
52 | return apolloClient
53 | }
54 |
--------------------------------------------------------------------------------
/lib/initRedux.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
2 | import reducers from './reducers'
3 |
4 | let reduxStore = null
5 |
6 | // Get the Redux DevTools extension and fallback to a no-op function
7 | let devtools = f => f
8 | if (process.browser && window.__REDUX_DEVTOOLS_EXTENSION__) {
9 | devtools = window.__REDUX_DEVTOOLS_EXTENSION__()
10 | }
11 |
12 | function create (apollo, initialState = {}) {
13 | return createStore(
14 | combineReducers({ // Setup reducers
15 | ...reducers,
16 | apollo: apollo.reducer()
17 | }),
18 | initialState, // Hydrate the store with server-side data
19 | compose(
20 | applyMiddleware(apollo.middleware()), // Add additional middleware here
21 | devtools
22 | )
23 | )
24 | }
25 |
26 | export default function initRedux (apollo, initialState) {
27 | // Make sure to create a new store for every server-side request so that data
28 | // isn't shared between connections (which would be bad)
29 | if (!process.browser) {
30 | return create(apollo, initialState)
31 | }
32 |
33 | // Reuse store on the client-side
34 | if (!reduxStore) {
35 | reduxStore = create(apollo, initialState)
36 | }
37 |
38 | return reduxStore
39 | }
40 |
--------------------------------------------------------------------------------
/lib/reducers.js:
--------------------------------------------------------------------------------
1 | export default {
2 | checkoutId(state = null, { type, payload }) {
3 | switch (type) {
4 | case 'SET_CHECKOUT_ID':
5 | return payload.id
6 | default:
7 | return state
8 | }
9 | },
10 | isCartOpen(state = false, { type, payload }) {
11 | switch (type) {
12 | case 'TOGGLE_CART':
13 | return !state
14 | case 'OPEN_CART':
15 | return true
16 | case 'CLOSE_CART':
17 | return false
18 | default:
19 | return state
20 | }
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/lib/withData.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { ApolloProvider, getDataFromTree } from 'react-apollo'
4 | import Head from 'next/head'
5 | import initApollo from './initApollo'
6 | import initRedux from './initRedux'
7 |
8 | // Gets the display name of a JSX component for dev tools
9 | function getComponentDisplayName (Component) {
10 | return Component.displayName || Component.name || 'Unknown'
11 | }
12 |
13 | export default ComposedComponent => {
14 | return class WithData extends React.Component {
15 | static displayName = `WithData(${getComponentDisplayName(ComposedComponent)})`
16 | static propTypes = {
17 | serverState: PropTypes.object.isRequired
18 | }
19 |
20 | static async getInitialProps (ctx) {
21 | let serverState = {}
22 |
23 | // Evaluate the composed component's getInitialProps()
24 | let composedInitialProps = {}
25 | if (ComposedComponent.getInitialProps) {
26 | composedInitialProps = await ComposedComponent.getInitialProps(ctx)
27 | }
28 |
29 | // Run all GraphQL queries in the component tree
30 | // and extract the resulting data
31 | if (!process.browser) {
32 | const apollo = initApollo()
33 | const redux = initRedux(apollo)
34 | // Provide the `url` prop data in case a GraphQL query uses it
35 | const url = {query: ctx.query, pathname: ctx.pathname}
36 |
37 | try {
38 | // Run all GraphQL queries
39 | await getDataFromTree(
40 | // No need to use the Redux Provider
41 | // because Apollo sets up the store for us
42 |
43 |
44 |
45 | )
46 | } catch (error) {
47 | // Prevent Apollo Client GraphQL errors from crashing SSR.
48 | // Handle them in components via the data.error prop:
49 | // http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error
50 | }
51 | // getDataFromTree does not call componentWillUnmount
52 | // head side effect therefore need to be cleared manually
53 | Head.rewind()
54 |
55 | // Extract query data from the store
56 | const state = redux.getState()
57 |
58 | // No need to include other initial Redux state because when it
59 | // initialises on the client-side it'll create it again anyway
60 | serverState = {
61 | apollo: { // Only include the Apollo data state
62 | data: state.apollo.data
63 | }
64 | }
65 | }
66 |
67 | return {
68 | serverState,
69 | ...composedInitialProps
70 | }
71 | }
72 |
73 | constructor (props) {
74 | super(props)
75 | this.apollo = initApollo()
76 | this.redux = initRedux(this.apollo, this.props.serverState)
77 | }
78 |
79 | render () {
80 | return (
81 | // No need to use the Redux Provider
82 | // because Apollo sets up the store for us
83 |
84 |
85 |
86 | )
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-apollo",
3 | "version": "2.0.0",
4 | "scripts": {
5 | "dev": "next",
6 | "build": "next build",
7 | "start": "next start",
8 | "storybook": "start-storybook -p 6006",
9 | "build-storybook": "build-storybook"
10 | },
11 | "dependencies": {
12 | "graphql": "^0.9.3",
13 | "isomorphic-fetch": "^2.2.1",
14 | "next": "^3.0.1-beta.18",
15 | "prop-types": "^15.5.8",
16 | "react": "^15.5.4",
17 | "react-apollo": "^1.1.3",
18 | "react-dom": "^15.5.4",
19 | "react-redux": "^5.0.5",
20 | "recompose": "^0.24.0",
21 | "styled-components": "^2.1.2"
22 | },
23 | "author": "",
24 | "license": "ISC",
25 | "devDependencies": {
26 | "@storybook/react": "^3.1.8"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import App from '../components/App'
2 | import Header from '../components/Header'
3 | import Submit from '../components/Submit'
4 | import PostList from '../components/PostList'
5 | import ProductList from '../components/ProductList'
6 | import withData from '../lib/withData'
7 |
8 | export default withData((props) => (
9 |
10 |
11 |
12 | ))
13 |
--------------------------------------------------------------------------------
/pages/product.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactifyJS/Reactify/7cbb3165893acd2994792ef5c42d82ea7cf7167e/pages/product.js
--------------------------------------------------------------------------------
/stories/data/checkout.js:
--------------------------------------------------------------------------------
1 | export const emptyCheckout = {
2 | "id": "Z2lkOi8vc2hvcGlmeS9DaGVja291dC9mNjNlMzZlYWEyNDNiMjQyZTRkZGRmZGE5NGJmYjdhZT9rZXk9NjA0NjFhNGJjOWZjYzg1YWVlYTY2MzlhZmUwNDkxOGI=",
3 | "webUrl": "https://checkout.shopify.com/13120893/checkouts/f63e36eaa243b242e4dddfda94bfb7ae?key=60461a4bc9fcc85aeea6639afe04918b",
4 | "totalTax": "0.00",
5 | "subtotalPrice": "0.00",
6 | "totalPrice": "0.00",
7 | "lineItems": {
8 | "edges": [],
9 | }
10 | };
11 |
12 | export const fullCheckout = {
13 | "id": "Z2lkOi8vc2hvcGlmeS9DaGVja291dC9mNjNlMzZlYWEyNDNiMjQyZTRkZGRmZGE5NGJmYjdhZT9rZXk9NjA0NjFhNGJjOWZjYzg1YWVlYTY2MzlhZmUwNDkxOGI=",
14 | "webUrl": "https://checkout.shopify.com/13120893/checkouts/f63e36eaa243b242e4dddfda94bfb7ae?key=60461a4bc9fcc85aeea6639afe04918b",
15 | "totalTax": "24.20",
16 | "subtotalPrice": "484.00",
17 | "totalPrice": "508.20",
18 | "lineItems": {
19 | "edges": [
20 | {
21 | "node": {
22 | "id": "Z2lkOi8vc2hvcGlmeS9DaGVja291dExpbmVJdGVtLzFlNjFjMGM3NDBkN2ZkMjg5ZDcyZDMzODhhYmVmMGI1P2NoZWNrb3V0PWY2M2UzNmVhYTI0M2IyNDJlNGRkZGZkYTk0YmZiN2Fl",
23 | "title": "Hanra Shirt",
24 | "variant": {
25 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzcxMjI1OQ==",
26 | "title": "Grey / S",
27 | "image": {
28 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_c0f47f6b-eddb-494e-9915-483d4153b0a1.jpg?v=1491851030",
29 | },
30 | "price": "108.00",
31 | },
32 | "quantity": 1,
33 | },
34 | },
35 | {
36 | "node": {
37 | "id": "Z2lkOi8vc2hvcGlmeS9DaGVja291dExpbmVJdGVtLzg4ZDczNTFiOTAxNGZmM2YxY2Y0YmNmYzQ5NTkxZjI3P2NoZWNrb3V0PWY2M2UzNmVhYTI0M2IyNDJlNGRkZGZkYTk0YmZiN2Fl",
38 | "title": "Neptune Boot",
39 | "variant": {
40 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzNzYzNQ==",
41 | "title": "Black / 7",
42 | "image": {
43 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
44 | },
45 | "price": "188.00",
46 | },
47 | "quantity": 2,
48 | },
49 | }
50 | ],
51 | }
52 | };
--------------------------------------------------------------------------------
/stories/data/lineItem.js:
--------------------------------------------------------------------------------
1 | const lineItem = {
2 | "id": "Z2lkOi8vc2hvcGlmeS9DaGVja291dExpbmVJdGVtLzFlNjFjMGM3NDBkN2ZkMjg5ZDcyZDMzODhhYmVmMGI1P2NoZWNrb3V0PWY2M2UzNmVhYTI0M2IyNDJlNGRkZGZkYTk0YmZiN2Fl",
3 | "title": "Hanra Shirt",
4 | "variant": {
5 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzcxMjI1OQ==",
6 | "title": "Grey / S",
7 | "image": {
8 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_c0f47f6b-eddb-494e-9915-483d4153b0a1.jpg?v=1491851030",
9 | "__typename": "Image"
10 | },
11 | "price": "108.00",
12 | "__typename": "ProductVariant"
13 | },
14 | "quantity": 1,
15 | "__typename": "CheckoutLineItem"
16 | }
17 |
18 | export default lineItem;
--------------------------------------------------------------------------------
/stories/data/option.js:
--------------------------------------------------------------------------------
1 | const option = {
2 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEyMDMxMjM3Mzc5",
3 | "name": "Size",
4 | "values": [
5 | "S",
6 | "M",
7 | "L"
8 | ],
9 | "__typename": "ProductOption"
10 | }
11 |
12 | export default option;
--------------------------------------------------------------------------------
/stories/data/product.js:
--------------------------------------------------------------------------------
1 | const sampleProduct = {
2 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzk4OTUyNzkwNDM=",
3 | "title": "Neptune Boot",
4 | "options": [
5 | {
6 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEyMDMxMjIyODUx",
7 | "name": "Color",
8 | "values": [
9 | "Black",
10 | "Charcoal"
11 | ],
12 | "__typename": "ProductOption"
13 | },
14 | {
15 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEyMDMxMjIyOTE1",
16 | "name": "Size",
17 | "values": [
18 | "7",
19 | "8",
20 | "9",
21 | "10",
22 | "11"
23 | ],
24 | "__typename": "ProductOption"
25 | }
26 | ],
27 | "variants": {
28 | "pageInfo": {
29 | "hasNextPage": false,
30 | "hasPreviousPage": false,
31 | "__typename": "PageInfo"
32 | },
33 | "edges": [
34 | {
35 | "node": {
36 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzNzYzNQ==",
37 | "title": "Black / 7",
38 | "selectedOptions": [
39 | {
40 | "name": "Color",
41 | "value": "Black",
42 | "__typename": "SelectedOption"
43 | },
44 | {
45 | "name": "Size",
46 | "value": "7",
47 | "__typename": "SelectedOption"
48 | }
49 | ],
50 | "image": {
51 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
52 | "__typename": "Image"
53 | },
54 | "price": "188.00",
55 | "__typename": "ProductVariant"
56 | },
57 | "__typename": "ProductVariantEdge"
58 | },
59 | {
60 | "node": {
61 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzNzY5OQ==",
62 | "title": "Black / 8",
63 | "selectedOptions": [
64 | {
65 | "name": "Color",
66 | "value": "Black",
67 | "__typename": "SelectedOption"
68 | },
69 | {
70 | "name": "Size",
71 | "value": "8",
72 | "__typename": "SelectedOption"
73 | }
74 | ],
75 | "image": {
76 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
77 | "__typename": "Image"
78 | },
79 | "price": "188.00",
80 | "__typename": "ProductVariant"
81 | },
82 | "__typename": "ProductVariantEdge"
83 | },
84 | {
85 | "node": {
86 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzNzc2Mw==",
87 | "title": "Black / 9",
88 | "selectedOptions": [
89 | {
90 | "name": "Color",
91 | "value": "Black",
92 | "__typename": "SelectedOption"
93 | },
94 | {
95 | "name": "Size",
96 | "value": "9",
97 | "__typename": "SelectedOption"
98 | }
99 | ],
100 | "image": {
101 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
102 | "__typename": "Image"
103 | },
104 | "price": "188.00",
105 | "__typename": "ProductVariant"
106 | },
107 | "__typename": "ProductVariantEdge"
108 | },
109 | {
110 | "node": {
111 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzNzgyNw==",
112 | "title": "Black / 10",
113 | "selectedOptions": [
114 | {
115 | "name": "Color",
116 | "value": "Black",
117 | "__typename": "SelectedOption"
118 | },
119 | {
120 | "name": "Size",
121 | "value": "10",
122 | "__typename": "SelectedOption"
123 | }
124 | ],
125 | "image": {
126 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
127 | "__typename": "Image"
128 | },
129 | "price": "188.00",
130 | "__typename": "ProductVariant"
131 | },
132 | "__typename": "ProductVariantEdge"
133 | },
134 | {
135 | "node": {
136 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzNzg5MQ==",
137 | "title": "Black / 11",
138 | "selectedOptions": [
139 | {
140 | "name": "Color",
141 | "value": "Black",
142 | "__typename": "SelectedOption"
143 | },
144 | {
145 | "name": "Size",
146 | "value": "11",
147 | "__typename": "SelectedOption"
148 | }
149 | ],
150 | "image": {
151 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
152 | "__typename": "Image"
153 | },
154 | "price": "188.00",
155 | "__typename": "ProductVariant"
156 | },
157 | "__typename": "ProductVariantEdge"
158 | },
159 | {
160 | "node": {
161 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzODAxOQ==",
162 | "title": "Charcoal / 7",
163 | "selectedOptions": [
164 | {
165 | "name": "Color",
166 | "value": "Charcoal",
167 | "__typename": "SelectedOption"
168 | },
169 | {
170 | "name": "Size",
171 | "value": "7",
172 | "__typename": "SelectedOption"
173 | }
174 | ],
175 | "image": {
176 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/003_90773333-cbea-4183-9324-41ffbba3d89b.jpg?v=1491850944",
177 | "__typename": "Image"
178 | },
179 | "price": "188.00",
180 | "__typename": "ProductVariant"
181 | },
182 | "__typename": "ProductVariantEdge"
183 | },
184 | {
185 | "node": {
186 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzODA4Mw==",
187 | "title": "Charcoal / 8",
188 | "selectedOptions": [
189 | {
190 | "name": "Color",
191 | "value": "Charcoal",
192 | "__typename": "SelectedOption"
193 | },
194 | {
195 | "name": "Size",
196 | "value": "8",
197 | "__typename": "SelectedOption"
198 | }
199 | ],
200 | "image": {
201 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/003_90773333-cbea-4183-9324-41ffbba3d89b.jpg?v=1491850944",
202 | "__typename": "Image"
203 | },
204 | "price": "188.00",
205 | "__typename": "ProductVariant"
206 | },
207 | "__typename": "ProductVariantEdge"
208 | },
209 | {
210 | "node": {
211 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzODE0Nw==",
212 | "title": "Charcoal / 9",
213 | "selectedOptions": [
214 | {
215 | "name": "Color",
216 | "value": "Charcoal",
217 | "__typename": "SelectedOption"
218 | },
219 | {
220 | "name": "Size",
221 | "value": "9",
222 | "__typename": "SelectedOption"
223 | }
224 | ],
225 | "image": {
226 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/003_90773333-cbea-4183-9324-41ffbba3d89b.jpg?v=1491850944",
227 | "__typename": "Image"
228 | },
229 | "price": "188.00",
230 | "__typename": "ProductVariant"
231 | },
232 | "__typename": "ProductVariantEdge"
233 | },
234 | {
235 | "node": {
236 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzODI3NQ==",
237 | "title": "Charcoal / 10",
238 | "selectedOptions": [
239 | {
240 | "name": "Color",
241 | "value": "Charcoal",
242 | "__typename": "SelectedOption"
243 | },
244 | {
245 | "name": "Size",
246 | "value": "10",
247 | "__typename": "SelectedOption"
248 | }
249 | ],
250 | "image": {
251 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/003_90773333-cbea-4183-9324-41ffbba3d89b.jpg?v=1491850944",
252 | "__typename": "Image"
253 | },
254 | "price": "188.00",
255 | "__typename": "ProductVariant"
256 | },
257 | "__typename": "ProductVariantEdge"
258 | },
259 | {
260 | "node": {
261 | "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zNjYwNzYzODMzOQ==",
262 | "title": "Charcoal / 11",
263 | "selectedOptions": [
264 | {
265 | "name": "Color",
266 | "value": "Charcoal",
267 | "__typename": "SelectedOption"
268 | },
269 | {
270 | "name": "Size",
271 | "value": "11",
272 | "__typename": "SelectedOption"
273 | }
274 | ],
275 | "image": {
276 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/003_90773333-cbea-4183-9324-41ffbba3d89b.jpg?v=1491850944",
277 | "__typename": "Image"
278 | },
279 | "price": "188.00",
280 | "__typename": "ProductVariant"
281 | },
282 | "__typename": "ProductVariantEdge"
283 | }
284 | ],
285 | "__typename": "ProductVariantConnection"
286 | },
287 | "images": {
288 | "pageInfo": {
289 | "hasNextPage": false,
290 | "hasPreviousPage": false,
291 | "__typename": "PageInfo"
292 | },
293 | "edges": [
294 | {
295 | "node": {
296 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/001_f5b2f018-5434-446e-912c-10484293c134.jpg?v=1491850944",
297 | "__typename": "Image"
298 | },
299 | "__typename": "ImageEdge"
300 | },
301 | {
302 | "node": {
303 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/002_0920679e-5437-4ad1-a9a2-ffc3339bbd0e.jpg?v=1491850944",
304 | "__typename": "Image"
305 | },
306 | "__typename": "ImageEdge"
307 | },
308 | {
309 | "node": {
310 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/003_90773333-cbea-4183-9324-41ffbba3d89b.jpg?v=1491850944",
311 | "__typename": "Image"
312 | },
313 | "__typename": "ImageEdge"
314 | },
315 | {
316 | "node": {
317 | "src": "https://cdn.shopify.com/s/files/1/1312/0893/products/004_98784e81-b5bc-407d-add0-fae03da1f45f.jpg?v=1491850944",
318 | "__typename": "Image"
319 | },
320 | "__typename": "ImageEdge"
321 | }
322 | ],
323 | "__typename": "ImageConnection"
324 | },
325 | "__typename": "Product"
326 | }
327 |
328 | export default sampleProduct;
--------------------------------------------------------------------------------
/stories/index 10.44.39.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { storiesOf } from '@storybook/react';
4 | import { action } from '@storybook/addon-actions';
5 | import { linkTo } from '@storybook/addon-links';
6 |
7 | import { Button, Welcome } from '@storybook/react/demo';
8 |
9 | storiesOf('Welcome', module).add('to Storybook', () => );
10 |
11 | storiesOf('Button', module)
12 | .add('with text', () => )
13 | .add('with some emoji', () => );
14 |
--------------------------------------------------------------------------------
/stories/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { storiesOf } from '@storybook/react';
4 | import { action } from '@storybook/addon-actions';
5 | import { linkTo } from '@storybook/addon-links';
6 |
7 | import { Button, Welcome } from '@storybook/react/demo';
8 |
9 | // import Cart from '../components/Cart.js';
10 | import CustomerAuth from '../components/CustomerAuth.js';
11 | import LineItem from '../components/LineItem.js';
12 | import { Product } from '../components/Product.js';
13 | import VariantSelector from '../components/VariantSelector.js';
14 |
15 | import './storybook.css';
16 |
17 | import {emptyCheckout, fullCheckout} from './data/checkout.js';
18 | import sampleProduct from './data/product.js';
19 | import sampleLineItem from './data/lineItem.js';
20 | import sampleOption from './data/option.js';
21 |
22 | // storiesOf('Cart', module)
23 | // .add('empty', () => )
24 | // .add('full', () => )
25 |
26 | // storiesOf('CustomerAuth', module)
27 | // .add('default', () => )
28 |
29 | storiesOf('LineItem', module)
30 | .add('default', () => )
31 |
32 | storiesOf('Product', module)
33 | .add('default', () => )
34 |
35 | storiesOf('VariantSelector', module)
36 | .add('default', () => )
--------------------------------------------------------------------------------
/stories/storybook.css:
--------------------------------------------------------------------------------
1 | /* INITIALIZERS + DEFAULTS
2 | * ============================== */
3 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,700');
4 |
5 | *, *:before, *:after {
6 | box-sizing: border-box;
7 | }
8 |
9 | html {
10 | font-size: 65%;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | padding: 0;
16 | font-family: 'Roboto', sans-serif;
17 | font-weight: 400;
18 | }
19 |
20 | img {
21 | display: block;
22 | max-width: 100%;
23 | max-height: 100%;
24 | }
25 |
26 | h1 {
27 | font-weight: 300;
28 | margin: 0 0 15px;
29 | font-size: 3rem;
30 | }
31 |
32 | h2 {
33 | font-weight: 300;
34 | margin: 0;
35 | font-size: 2rem;
36 | }
37 |
38 | /* BASE APP
39 | * ============================== */
40 | .App__header {
41 | background-color: #222;
42 | background-image: url('https://unsplash.it/1000/300?image=823');
43 | background-size: cover;
44 | color: white;
45 | padding: 10px 10px;
46 | }
47 |
48 | .App__nav{
49 | width: 100%;
50 | list-style: none;
51 | }
52 |
53 | .App__customer-actions {
54 | float: left;
55 | padding: 10px;
56 | }
57 |
58 | .App__title {
59 | padding: 80px 20px;
60 | text-align: center;
61 | }
62 |
63 | .Product-wrapper {
64 | max-width: 900px;
65 | margin: 40px auto 0;
66 | display: flex;
67 | flex-wrap: wrap;
68 | justify-content: center;
69 | }
70 |
71 | .App__view-cart-wrapper {
72 | float: right;
73 | }
74 |
75 | .App__view-cart {
76 | font-size: 15px;
77 | border: none;
78 | background: none;
79 | cursor: pointer;
80 | color: white;
81 | }
82 |
83 | .button {
84 | background-color: #2752ff;
85 | color: white;
86 | border: none;
87 | font-size: 1.2rem;
88 | padding: 10px 17px;
89 | cursor: pointer;
90 | }
91 |
92 | .button:hover,
93 | .button:focus {
94 | background-color: #222222;
95 | }
96 |
97 | .button:disabled {
98 | background: #bfbfbf;
99 | cursor: not-allowed;
100 | }
101 |
102 | .login {
103 | font-size: 1.2rem;
104 | color: #b8b8b8;
105 | cursor: pointer;
106 | }
107 |
108 | .login:hover {
109 | color: white;
110 | }
111 |
112 | .Flash__message-wrapper {
113 | -webkit-justify-content: center;
114 | -ms-flex-pack: center;
115 | align-items: flex-end;
116 | justify-content: center;
117 | position: fixed;
118 | bottom: 0;
119 | pointer-events: none;
120 | z-index: 227;
121 | left: 50%;
122 | transform: translateX(-50%);
123 | }
124 |
125 | .Flash__message {
126 | background: rgba(0,0,0,0.88);
127 | border-radius: 3px;
128 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
129 | color: #ffffff;
130 | cursor: default;
131 | display: -webkit-flex;
132 | display: -ms-flexbox;
133 | display: none;
134 | pointer-events: auto;
135 | position: relative;
136 | font-size: 20px;
137 | line-height: 28px;
138 | font-weight: 400;
139 | padding: 10px 20px;
140 | margin: 0;
141 | }
142 |
143 | .Flash__message--open {
144 | display: flex;
145 | }
146 |
147 | /* CART
148 | * ============================== */
149 | .Cart {
150 | position: fixed;
151 | top: 0;
152 | right: 0;
153 | height: 100%;
154 | width: 350px;
155 | background-color: white;
156 | display: flex;
157 | flex-direction: column;
158 | border-left: 1px solid #e5e5e5;
159 | transform: translateX(100%);
160 | transition: transform 0.15s ease-in-out;
161 | }
162 |
163 | .Cart--open {
164 | transform: translateX(0);
165 | }
166 |
167 | .Cart__close {
168 | position: absolute;
169 | right: 9px;
170 | top: 8px;
171 | font-size: 35px;
172 | color: #999;
173 | border: none;
174 | background: transparent;
175 | transition: transform 100ms ease;
176 | cursor: pointer;
177 | }
178 |
179 | .Cart__header {
180 | padding: 20px;
181 | border-bottom: 1px solid #e5e5e5;
182 | flex: 0 0 auto;
183 | display: inline-block;
184 | }
185 |
186 | .Cart__line-items {
187 | flex: 1 0 auto;
188 | margin: 0;
189 | padding: 20px;
190 | }
191 |
192 | .Cart__footer {
193 | padding: 20px;
194 | border-top: 1px solid #e5e5e5;
195 | flex: 0 0 auto;
196 | }
197 |
198 | .Cart__checkout {
199 | margin-top: 20px;
200 | display: block;
201 | width: 100%;
202 | }
203 |
204 | .Cart-info {
205 | padding: 15px 20px 10px;
206 | }
207 |
208 | .Cart-info__total {
209 | float: left;
210 | text-transform: uppercase;
211 | }
212 |
213 | .Cart-info__small {
214 | font-size: 11px;
215 | }
216 |
217 | .Cart-info__pricing {
218 | float: right;
219 | }
220 |
221 | .pricing {
222 | margin-left: 5px;
223 | font-size: 16px;
224 | color: black;
225 | }
226 |
227 | /* LINE ITEMS
228 | * ============================== */
229 | .Line-item {
230 | margin-bottom: 20px;
231 | overflow: hidden;
232 | backface-visibility: visible;
233 | min-height: 65px;
234 | position: relative;
235 | opacity: 1;
236 | transition: opacity 0.2s ease-in-out;
237 | }
238 |
239 | .Line-item__img {
240 | width: 65px;
241 | height: 65px;
242 | border-radius: 3px;
243 | background-size: contain;
244 | background-repeat: no-repeat;
245 | background-position: center center;
246 | background-color: #e5e5e5;
247 | position: absolute;
248 | }
249 |
250 | .Line-item__content {
251 | width: 100%;
252 | padding-left: 75px;
253 | }
254 |
255 | .Line-item__content-row {
256 | display: inline-block;
257 | width: 100%;
258 | margin-bottom: 5px;
259 | position: relative;
260 | }
261 |
262 | .Line-item__variant-title {
263 | float: right;
264 | font-weight: bold;
265 | font-size: 11px;
266 | line-height: 17px;
267 | color: #767676;
268 | }
269 |
270 | .Line-item__title {
271 | color: #4E5665;
272 | font-size: 15px;
273 | font-weight: 400;
274 | }
275 |
276 | .Line-item__price {
277 | line-height: 23px;
278 | float: right;
279 | font-weight: bold;
280 | font-size: 15px;
281 | margin-right: 40px;
282 | }
283 |
284 | .Line-item__quantity-container {
285 | border: 1px solid #767676;
286 | float: left;
287 | border-radius: 3px;
288 | }
289 |
290 | .Line-item__quantity-update {
291 | color: #767676;
292 | display: block;
293 | float: left;
294 | height: 21px;
295 | line-height: 16px;
296 | font-family: monospace;
297 | width: 25px;
298 | padding: 0;
299 | border: none;
300 | background: transparent;
301 | box-shadow: none;
302 | cursor: pointer;
303 | font-size: 18px;
304 | text-align: center;
305 | }
306 |
307 | .Line-item__quantity-update-form {
308 | display: inline;
309 | }
310 |
311 | .Line-item__quantity {
312 | color: black;
313 | width: 38px;
314 | height: 21px;
315 | line-height: 23px;
316 | font-size: 15px;
317 | border: none;
318 | text-align: center;
319 | -moz-appearance: textfield;
320 | background: transparent;
321 | border-left: 1px solid #767676;
322 | border-right: 1px solid #767676;
323 | display: block;
324 | float: left;
325 | padding: 0;
326 | border-radius: 0;
327 | }
328 |
329 | .Line-item__remove {
330 | position: absolute;
331 | right: 0;
332 | top: 0;
333 | border: 0;
334 | background: 0;
335 | font-size: 20px;
336 | top: -4px;
337 | opacity: 0.5;
338 | }
339 |
340 | .Line-item__remove:hover {
341 | opacity: 1;
342 | cursor: pointer;
343 | }
344 |
345 | /* PRODUCTS
346 | * ============================== */
347 | .Product {
348 | flex: 0 1 31%;
349 | margin-left: 1%;
350 | margin-right: 1%;
351 | margin-bottom: 3%;
352 | }
353 |
354 | .Product__title {
355 | font-size: 1.3rem;
356 | margin-top: 1rem;
357 | margin-bottom: 0.4rem;
358 | opacity: 0.7;
359 | }
360 |
361 | .Product__price {
362 | display: block;
363 | font-size: 1.1rem;
364 | opacity: 0.5;
365 | margin-bottom: 0.4rem;
366 | }
367 |
368 | .Product__option {
369 | display: block;
370 | width: 100%;
371 | margin-bottom: 10px;
372 | }
373 |
374 | .Product__quantity {
375 | display: block;
376 | width: 15%;
377 | margin-bottom: 10px;
378 | }
379 |
380 | /* CUSTOMER AUTH
381 | * ============================== */
382 | .CustomerAuth {
383 | background: #2a2c2e;
384 | display: none;
385 | height: 100%;
386 | left: 0;
387 | opacity: 0;
388 | padding: 0 0 65px;
389 | top: 0;
390 | width: 100%;
391 | text-align: center;
392 | color: #fff;
393 | transition: opacity 150ms;
394 | opacity: 1;
395 | visibility: visible;
396 | z-index: 1000;
397 | position: fixed;
398 | }
399 |
400 | .CustomerAuth--open {
401 | display: block;
402 | }
403 |
404 | .CustomerAuth__close {
405 | position: absolute;
406 | right: 9px;
407 | top: 8px;
408 | font-size: 35px;
409 | color: #999;
410 | border: none;
411 | background: transparent;
412 | transition: transform 100ms ease;
413 | cursor: pointer;
414 | }
415 |
416 | .CustomerAuth__body {
417 | padding: 130px 30px;
418 | width: 700px;
419 | margin-left: auto;
420 | margin-right: auto;
421 | text-align: left;
422 | position: relative;
423 | }
424 |
425 | .CustomerAuth__heading {
426 | font-size: 24px;
427 | font-weight: 500;
428 | padding-bottom: 15px;
429 | }
430 |
431 | .CustomerAuth__credential {
432 | display: block;
433 | position: relative;
434 | margin-bottom: 15px;
435 | border-radius: 3px;
436 | }
437 |
438 | .CustomerAuth__input {
439 | height: 60px;
440 | padding: 24px 10px 20px;
441 | border: 0px;
442 | font-size: 18px;
443 | background: #fff;
444 | width: 100%;
445 | }
446 |
447 | .CustomerAuth__submit {
448 | float: right;
449 | }
450 |
451 | .error {
452 | display: block;
453 | font-size: 15px;
454 | padding: 10px;
455 | position: relative;
456 | min-height: 2.64286em;
457 | background: #fbefee;
458 | color: #c23628;
459 | }
460 | .Product img{
461 | max-width: 200px;
462 | }
463 |
464 | .Product img{
465 | max-height: 300px;
466 | }
--------------------------------------------------------------------------------