├── .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 | ![https://d3vv6lp55qjaqc.cloudfront.net/items/153G2k1T1U443t1D1z2L/reactify-logo-2.png?X-CloudApp-Visitor-Id=d40749865873d7b5ab32c80852150f74&v=8b3ec22a](https://d3vv6lp55qjaqc.cloudfront.net/items/153G2k1T1U443t1D1z2L/reactify-logo-2.png?X-CloudApp-Visitor-Id=d40749865873d7b5ab32c80852150f74&v=8b3ec22a) 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 |
60 |
61 |
Subtotal
62 |
63 | $ {this.props.checkout.subtotalPrice} 64 |
65 |
66 |
67 |
Taxes
68 |
69 | $ {this.props.checkout.totalTax} 70 |
71 |
72 |
73 |
Total
74 |
75 | $ {this.props.checkout.totalPrice} 76 |
77 |
78 | 79 |
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 |
9 |
    10 |
11 |
12 |

{shop && shop.name}: React Example

13 |

{shop && shop.description}

14 |
15 | 16 |
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 ? {`${this.props.line_item.title} : 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 |
      17 | {index + 1}. 18 | {post.title} 19 | 20 |
      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 ? {`${this.props.product.title} : 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 |
    29 |

    Submit

    30 | 31 | 32 | 33 | 47 |
    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 | } --------------------------------------------------------------------------------