├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── build └── babelRelayPlugin.js ├── data ├── defaultDefinitions.js ├── models │ ├── Cart.js │ ├── CartEntry.js │ ├── Product.js │ └── ProductList.js ├── mutations │ ├── addToCartMutation.js │ └── removeFromCartMutation.js ├── schema.graphqls ├── schema.js ├── schema.json ├── services │ ├── cartService.js │ └── productService.js └── types │ ├── cartEntryType.js │ ├── cartType.js │ ├── imageType.js │ ├── productListType.js │ ├── productType.js │ ├── productsType.js │ └── viewerType.js ├── graphql.config.json ├── graphql.schema.json ├── gulpfile.babel.js ├── js ├── app.js ├── components │ ├── App.js │ ├── cart │ │ ├── Cart.js │ │ ├── Cart.scss │ │ ├── CartEntry.js │ │ ├── CartEntry.scss │ │ ├── CartWidget.js │ │ └── CartWidget.scss │ ├── common │ │ ├── Ball.js │ │ ├── CurveBall.js │ │ ├── CurveBall.scss │ │ ├── Portal.js │ │ ├── Portal.scss │ │ ├── Price.js │ │ ├── Price.scss │ │ ├── Scroll.js │ │ └── Scroll.scss │ ├── form │ │ ├── InputQuantity.js │ │ └── InputQuantity.scss │ ├── product │ │ ├── ProductEntry.js │ │ ├── ProductEntry.scss │ │ ├── ProductList.js │ │ └── ProductList.scss │ └── styles │ │ ├── animations.scss │ │ ├── animations │ │ ├── fade.scss │ │ ├── scale.scss │ │ ├── shake.scss │ │ └── slide.scss │ │ ├── common.scss │ │ ├── entries.scss │ │ ├── icons.scss │ │ ├── shape.scss │ │ └── variables.scss ├── mutations │ ├── AddToCartMutation.js │ └── RemoveFromCartMutation.js ├── queries │ └── viewerQueries.js ├── routes │ ├── cartRoute.js │ ├── productListRoute.js │ └── rootRoute.js └── utils │ └── RegExes.js ├── logger.js ├── package.json ├── postcss.config.js ├── public └── index.html ├── scripts └── updateSchema.js ├── server.js ├── tasks └── dev.js └── webpack.dev.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "passPerPreset": true, 3 | "presets": [ 4 | { 5 | "plugins": [ 6 | "./build/babelRelayPlugin" 7 | ] 8 | }, 9 | "react", 10 | ["es2015", {"loose": true}], 11 | "stage-0" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | 16 | [COMMIT_EDITMSG] 17 | max_line_length = 0 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /logs 3 | /dist 4 | .idea 5 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # relay-cart 2 | A simple shopping cart example leveraging relay & GraphQL with routing and pagination 3 | 4 | ## Usage 5 | clone this repo and run: 6 | 7 | ```shell 8 | npm install 9 | npm start 10 | ``` 11 | and then visit [http://localhost:3000/](http://localhost:3000/) 12 | 13 | ## Demo 14 | 15 | View a demo here: [http://120.76.218.113/relay-cart/demo.html](http://120.76.218.113/relay-cart/demo.html). 16 | Add items to the cart and change the quantities. 17 | 18 | ## Developing 19 | 20 | Any changes to files in the 'js' directory the server to automatically rebuild the app and refresh your browser. 21 | If at any time you make changes to data/schema.js, stop the server, regenerate data/schema.json, and restart the server: 22 | 23 | ```shell 24 | npm run updateSchema 25 | npm start 26 | ``` 27 | -------------------------------------------------------------------------------- /build/babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | var getBabelRelayPlugin = require('babel-relay-plugin'); 2 | var schema = require('../data/schema.json'); 3 | 4 | module.exports = getBabelRelayPlugin(schema.data); 5 | -------------------------------------------------------------------------------- /data/defaultDefinitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | 5 | import { 6 | GraphQLID, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLObjectType, 10 | GraphQLSchema, 11 | GraphQLString, 12 | } from 'graphql'; 13 | 14 | import { 15 | connectionArgs, 16 | connectionDefinitions, 17 | connectionFromArray, 18 | fromGlobalId, 19 | globalIdField, 20 | mutationWithClientMutationId, 21 | nodeDefinitions, 22 | } from 'graphql-relay'; 23 | 24 | import logger from '../logger'; 25 | 26 | import { productType, productListType, cartType, cartEntryType } from './schema'; 27 | 28 | import CartEntry from './models/CartEntry'; 29 | import cartService from './services/cartService'; 30 | 31 | import ProductList from './models/ProductList'; 32 | 33 | 34 | export const { nodeInterface, nodeField } = nodeDefinitions( 35 | async(globalId, session) => { 36 | logger.info(`Getting node data from globalId(${globalId})`); 37 | 38 | const { type, id } = fromGlobalId(globalId); 39 | 40 | if (type === 'CartEntry') { 41 | const cart = cartService.getSessionCart(session); 42 | return cart.entries.find((entry)=> entry.id === id); 43 | } 44 | 45 | if (type === 'ProductList') { 46 | return new ProductList({}); 47 | } 48 | 49 | return null; 50 | }, 51 | (obj) => { 52 | if (obj instanceof CartEntry) { 53 | return cartEntryType; 54 | } 55 | 56 | if (obj instanceof ProductList) { 57 | return productListType; 58 | } 59 | 60 | return null; 61 | } 62 | ); 63 | 64 | -------------------------------------------------------------------------------- /data/models/Cart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/26/16. 3 | */ 4 | 5 | export default class Cart { 6 | 7 | constructor({ entries }) { 8 | this.entries = entries; 9 | } 10 | 11 | get totalNumberOfItems() { 12 | let totalNumberOfItems = 0; 13 | if (this.entries.length > 0) { 14 | for (let i = 0; i < this.entries.length; ++i) { 15 | totalNumberOfItems += this.entries[i].quantity; 16 | } 17 | } 18 | 19 | return totalNumberOfItems; 20 | } 21 | 22 | get totalPriceOfItems() { 23 | let totalPriceOfItems = 0; 24 | if (this.entries.length > 0) { 25 | for (let i = 0; i < this.entries.length; ++i) { 26 | const entry = this.entries[i]; 27 | totalPriceOfItems += entry.quantity * entry.product.price; 28 | } 29 | } 30 | 31 | return totalPriceOfItems.toFixed(2); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /data/models/CartEntry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/26/16. 3 | */ 4 | 5 | export default class CartEntry { 6 | constructor({ id, product, quantity }) { 7 | this.id = id; 8 | this.product = product; 9 | this.quantity = quantity; 10 | this.price = product.price; 11 | this.totalPrice = (product.price * quantity).toFixed(2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /data/models/Product.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/25/16. 3 | */ 4 | 5 | const PRODUCTS = [ 6 | { 7 | id: '1118531647', productCode: '1118531647', 8 | name: 'JavaScript and JQuery: Interactive Front-End Web Development', 9 | price: 28.85, 10 | images: [{ 11 | format: 'thumbnail', 12 | url: 'http://ecx.images-amazon.com/images/I/41PhOmFQTTL._AA320_QL65_.jpg', 13 | }], 14 | }, 15 | { 16 | id: '0596517742', productCode: '0596517742', 17 | name: 'JavaScript: The Good Parts', 18 | price: 20.46, 19 | images: [{ 20 | format: 'thumbnail', 21 | url: 'http://ecx.images-amazon.com/images/I/518QVtPWA7L._AA320_QL65_.jpg', 22 | }], 23 | }, 24 | { 25 | id: '1593275404', productCode: '1593275404', 26 | name: 'The Principles of Object-Oriented JavaScript', 27 | price: 15.87, 28 | images: [{ 29 | format: 'thumbnail', 30 | url: 'http://ecx.images-amazon.com/images/I/51+Uy4JxjVL._AA320_QL65_.jpg', 31 | }], 32 | }, 33 | { 34 | id: '1593275846', productCode: '1593275846', 35 | name: 'Eloquent JavaScript: A Modern Introduction to Programming', 36 | price: 27.92, 37 | images: [{ 38 | format: 'thumbnail', 39 | url: 'http://ecx.images-amazon.com/images/I/51pLAgSXOzL._AA320_QL65_.jpg', 40 | }], 41 | }, 42 | { 43 | id: '1118026691', productCode: '1118026691', 44 | name: 'Professional JavaScript for Web Developers', 45 | price: 26.45, 46 | images: [{ 47 | format: 'thumbnail', 48 | url: 'http://ecx.images-amazon.com/images/I/51bRhyVTVGL._AA320_QL65_.jpg', 49 | }], 50 | }, 51 | { 52 | id: '1493692615', productCode: '1493692615', 53 | name: 'A Software Engineer Learns HTML5, JavaScript and jQuery', 54 | price: 13.49, 55 | images: [{ 56 | format: 'thumbnail', 57 | url: 'http://ecx.images-amazon.com/images/I/41XtnwhGs6L._AA320_QL65_.jpg', 58 | }], 59 | }, 60 | { 61 | id: '1495233006', productCode: '1495233006', 62 | name: 'Learn JavaScript VISUALLY', 63 | price: 13.46, 64 | images: [{ 65 | format: 'thumbnail', 66 | url: 'http://ecx.images-amazon.com/images/I/51F1TUdNOKL._AA320_QL65_.jpg', 67 | }], 68 | }, 69 | { 70 | id: '0596806752', productCode: '0596806752', 71 | name: 'JavaScript Patterns', 72 | price: 18.09, 73 | images: [{ 74 | format: 'thumbnail', 75 | url: 'http://ecx.images-amazon.com/images/I/51ACzMjH6rL._AA320_QL65_.jpg', 76 | }], 77 | }, 78 | { 79 | id: '1491924462', productCode: '1491924462', 80 | name: "You Don't Know JS: Up & Going", 81 | price: 4.99, 82 | images: [{ 83 | format: 'thumbnail', 84 | url: 'http://ecx.images-amazon.com/images/I/41FhogvNebL._AA320_QL65_.jpg', 85 | }], 86 | }, 87 | { 88 | id: '0321812182', productCode: '0321812182', 89 | name: 'Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript (Effective Software Development Series)', 90 | price: 29.48, 91 | images: [{ 92 | format: 'thumbnail', 93 | url: 'http://ecx.images-amazon.com/images/I/51t8vT-IvqL._AA320_QL65_.jpg', 94 | }], 95 | }, 96 | { 97 | id: '144934013X', productCode: '144934013X', 98 | name: 'Head First JavaScript Programming', 99 | price: 38.89, 100 | images: [{ 101 | format: 'thumbnail', 102 | url: 'http://ecx.images-amazon.com/images/I/51qpyuO-ANL._AA320_QL65_.jpg', 103 | }], 104 | }, 105 | { 106 | id: '1519638779', productCode: '1519638779', 107 | name: "JavaScript: Crash Course - The Ultimate Beginner's Course to Learning JavaScript Programming in Under 12 Hours", 108 | price: 9.99, 109 | images: [{ 110 | format: 'thumbnail', 111 | url: 'http://ecx.images-amazon.com/images/I/51d6QOwdxXL._AA320_QL65_.jpg', 112 | }], 113 | }, 114 | { 115 | id: '067233738X', productCode: '067233738X', 116 | name: 'JavaScript in 24 Hours, Sams Teach Yourself (6th Edition)', 117 | price: 27.95, 118 | images: [{ 119 | format: 'thumbnail', 120 | url: 'http://ecx.images-amazon.com/images/I/512+yH2PsbL._AA320_QL65_.jpg', 121 | }], 122 | }, 123 | { 124 | id: '152373082X', productCode: '152373082X', 125 | name: "JavaScript: The Ultimate Beginner's Guide!", 126 | price: 9.99, 127 | images: [{ 128 | format: 'thumbnail', 129 | url: 'http://ecx.images-amazon.com/images/I/41pLEp5yd7L._AA320_QL65_.jpg', 130 | }], 131 | }, 132 | ]; 133 | 134 | export default class Product { 135 | static getAll = () => PRODUCTS.map(p => new Product(p)); 136 | 137 | static findOne = ({ productCode }) => PRODUCTS.find(p => p.productCode === productCode); 138 | static findById = (id) => PRODUCTS.find(p => p.id === id); 139 | 140 | constructor({ id, name, price, color, images }) { 141 | this.id = id; 142 | this.name = name; 143 | this.color = color; 144 | this.price = price; 145 | this.images = images; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /data/models/ProductList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/25/16. 3 | */ 4 | 5 | import Product from './Product'; 6 | 7 | export default class ProductList { 8 | static findAll = ({ start, size })=> { 9 | const products = Product.getAll(); 10 | const items = products.slice(start, start + size); 11 | const totalNumberOfItems = products.length; 12 | 13 | return new ProductList({ 14 | items, 15 | totalNumberOfItems, 16 | }); 17 | }; 18 | 19 | constructor({ items, totalNumberOfItems }) { 20 | this.items = items; 21 | this.totalNumberOfItems = totalNumberOfItems; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /data/mutations/addToCartMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLList, 10 | GraphQLNonNull, 11 | GraphQLObjectType, 12 | GraphQLSchema, 13 | GraphQLInt, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | offsetToCursor, 27 | toGlobalId, 28 | } from 'graphql-relay'; 29 | 30 | import logger from '../../logger'; 31 | 32 | import cartEntryType, { cartEntryEdgeType } from '../types/cartEntryType'; 33 | import cartType from '../types/cartType'; 34 | 35 | import cartService from '../services/cartService'; 36 | import productService from '../services/productService'; 37 | 38 | const addToCartMutation = mutationWithClientMutationId({ 39 | name: 'AddToCart', 40 | inputFields: { 41 | id: { type: new GraphQLNonNull(GraphQLID) }, 42 | quantity: { type: GraphQLInt }, 43 | }, 44 | outputFields: { 45 | cartEntryEdge: { 46 | type: cartEntryEdgeType, 47 | }, 48 | cartEntry: { 49 | type: cartEntryType, 50 | }, 51 | cart: { 52 | type: cartType, 53 | }, 54 | }, 55 | mutateAndGetPayload: async({ id, quantity }, session) => { 56 | logger.info('Invoke addToCartMutation with params:', { id, quantity }); 57 | const cart = cartService.getSessionCart(session); 58 | 59 | const localProductId = fromGlobalId(id).id; 60 | const product = productService.findById(localProductId); 61 | 62 | const cartEntry = cartService.addToCart(cart, product.productCode, quantity); 63 | const cartEntryEdge = { 64 | cursor: cursorForObjectInConnection(cart.entries, cartEntry), 65 | node: cartEntry, 66 | }; 67 | 68 | return { cartEntry, cartEntryEdge, cart }; 69 | }, 70 | }); 71 | 72 | export default addToCartMutation; 73 | -------------------------------------------------------------------------------- /data/mutations/removeFromCartMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 3/1/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLList, 10 | GraphQLNonNull, 11 | GraphQLObjectType, 12 | GraphQLSchema, 13 | GraphQLInt, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | offsetToCursor, 27 | toGlobalId, 28 | } from 'graphql-relay'; 29 | 30 | import logger from '../../logger'; 31 | 32 | import cartService from '../services/cartService'; 33 | 34 | import cartType from '../types/cartType'; 35 | 36 | const removeFromCartMutation = mutationWithClientMutationId({ 37 | name: 'RemoveFromCart', 38 | inputFields: { 39 | id: { type: new GraphQLNonNull(GraphQLID) }, 40 | }, 41 | outputFields: { 42 | deletedCartEntryId: { 43 | type: GraphQLID, 44 | resolve: async({ deletedCartEntryId }) =>deletedCartEntryId, 45 | }, 46 | cart: { 47 | type: cartType, 48 | resolve: ({ cart })=> cart, 49 | }, 50 | }, 51 | mutateAndGetPayload: async({ id }, session) => { 52 | logger.info('Invoke removeFromCartMutation with params:', { id }); 53 | 54 | const cart = cartService.getSessionCart(session); 55 | const localCartEntryId = fromGlobalId(id).id; 56 | cartService.removeFromCart(cart, localCartEntryId); 57 | 58 | return { deletedCartEntryId: id, cart }; 59 | }, 60 | }); 61 | 62 | export default removeFromCartMutation; 63 | -------------------------------------------------------------------------------- /data/schema.graphqls: -------------------------------------------------------------------------------- 1 | input AddToCartInput { 2 | id: ID! 3 | quantity: Int 4 | clientMutationId: String 5 | } 6 | 7 | type AddToCartPayload { 8 | cartEntryEdge: CartEntryEdge 9 | cartEntry: CartEntry 10 | cart: Cart 11 | clientMutationId: String 12 | } 13 | 14 | type Cart implements Node { 15 | # The ID of an object 16 | id: ID! 17 | entries(after: String, first: Int, before: String, last: Int): CartEntryConnection 18 | totalNumberOfItems: Int 19 | totalPriceOfItems: Float 20 | } 21 | 22 | type CartEntry implements Node { 23 | # The ID of an object 24 | id: ID! 25 | product: Product 26 | quantity: Float 27 | price: Float 28 | totalPrice: Float 29 | } 30 | 31 | # A connection to a list of items. 32 | type CartEntryConnection { 33 | # Information to aid in pagination. 34 | pageInfo: PageInfo! 35 | 36 | # A list of edges. 37 | edges: [CartEntryEdge] 38 | } 39 | 40 | # An edge in a connection. 41 | type CartEntryEdge { 42 | # The item at the end of the edge 43 | node: CartEntry 44 | 45 | # A cursor for use in pagination 46 | cursor: String! 47 | } 48 | 49 | # Just image 50 | type Image { 51 | # The format of image. 52 | format: String 53 | 54 | # The url of image. 55 | url: String 56 | } 57 | 58 | type Mutation { 59 | addToCart(input: AddToCartInput!): AddToCartPayload 60 | removeFromCart(input: RemoveFromCartInput!): RemoveFromCartPayload 61 | } 62 | 63 | # An object with an ID 64 | interface Node { 65 | # The id of the object. 66 | id: ID! 67 | } 68 | 69 | # Information about pagination in a connection. 70 | type PageInfo { 71 | # When paginating forwards, are there more items? 72 | hasNextPage: Boolean! 73 | 74 | # When paginating backwards, are there more items? 75 | hasPreviousPage: Boolean! 76 | 77 | # When paginating backwards, the cursor to continue. 78 | startCursor: String 79 | 80 | # When paginating forwards, the cursor to continue. 81 | endCursor: String 82 | } 83 | 84 | type Product implements Node { 85 | # The ID of an object 86 | id: ID! 87 | 88 | # 商品名称 89 | name: String 90 | 91 | # 商品颜色 92 | color: String 93 | 94 | # 商品描述 95 | description: String 96 | 97 | # 商品价格,保留两位小数 98 | price: Float 99 | 100 | # 商品图片,['primary': 主图,'thumbnail':缩略图,'zoom':大图] 101 | images(format: String = "any", after: String, first: Int, before: String, last: Int): [Image] 102 | } 103 | 104 | # A connection to a list of items. 105 | type ProductConnection { 106 | # Information to aid in pagination. 107 | pageInfo: PageInfo! 108 | 109 | # A list of edges. 110 | edges: [ProductEdge] 111 | totalNumberOfItems: Int 112 | } 113 | 114 | # An edge in a connection. 115 | type ProductEdge { 116 | # The item at the end of the edge 117 | node: Product 118 | 119 | # A cursor for use in pagination 120 | cursor: String! 121 | } 122 | 123 | type ProductList implements Node { 124 | # The ID of an object 125 | id: ID! 126 | items(after: String, first: Int, before: String, last: Int): ProductConnection 127 | } 128 | 129 | type Query { 130 | product(id: ID): Product 131 | productList: ProductList 132 | cart: Cart 133 | viewer(id: ID): Viewer 134 | 135 | # Fetches an object given its ID 136 | node( 137 | # The ID of an object 138 | id: ID! 139 | ): Node 140 | } 141 | 142 | input RemoveFromCartInput { 143 | id: ID! 144 | clientMutationId: String 145 | } 146 | 147 | type RemoveFromCartPayload { 148 | deletedCartEntryId: ID 149 | cart: Cart 150 | clientMutationId: String 151 | } 152 | 153 | type Viewer implements Node { 154 | # The ID of an object 155 | id: ID! 156 | name: String 157 | cart: Cart 158 | products(after: String, first: Int, before: String, last: Int): ProductConnection 159 | } 160 | -------------------------------------------------------------------------------- /data/schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | 5 | import { 6 | GraphQLID, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLObjectType, 10 | GraphQLSchema, 11 | GraphQLString, 12 | } from 'graphql'; 13 | 14 | import { nodeField } from './defaultDefinitions'; 15 | export * from './defaultDefinitions'; 16 | 17 | import { viewerType, queryViewer } from './types/viewerType'; 18 | export * from './types/viewerType'; 19 | 20 | import { productType, queryProduct } from './types/productType'; 21 | export * from './types/productType'; 22 | 23 | import { productListType, queryProductList } from './types/productListType'; 24 | export * from './types/productListType'; 25 | 26 | import { cartType, queryCart } from './types/cartType'; 27 | export * from './types/cartType'; 28 | 29 | import { cartEntryType, queryCartEntry } from './types/cartEntryType'; 30 | export * from './types/cartEntryType'; 31 | 32 | 33 | const queryType = new GraphQLObjectType({ 34 | name: 'Query', 35 | fields: () => ({ 36 | 37 | product: queryProduct, 38 | productList: queryProductList, 39 | cart: queryCart, 40 | viewer: queryViewer, 41 | 42 | node: nodeField, 43 | }), 44 | }); 45 | 46 | const mutationType = new GraphQLObjectType({ 47 | name: 'Mutation', 48 | fields: () => ({ 49 | addToCart: require('./mutations/addToCartMutation').default, 50 | removeFromCart: require('./mutations/removeFromCartMutation').default, 51 | }), 52 | }); 53 | 54 | export const Schema = new GraphQLSchema({ 55 | query: queryType, 56 | mutation: mutationType, 57 | }); 58 | 59 | export default Schema; 60 | -------------------------------------------------------------------------------- /data/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": { 8 | "name": "Mutation" 9 | }, 10 | "subscriptionType": null, 11 | "types": [ 12 | { 13 | "kind": "OBJECT", 14 | "name": "Query", 15 | "description": null, 16 | "fields": [ 17 | { 18 | "name": "product", 19 | "description": null, 20 | "args": [ 21 | { 22 | "name": "id", 23 | "description": null, 24 | "type": { 25 | "kind": "SCALAR", 26 | "name": "ID", 27 | "ofType": null 28 | }, 29 | "defaultValue": null 30 | } 31 | ], 32 | "type": { 33 | "kind": "OBJECT", 34 | "name": "Product", 35 | "ofType": null 36 | }, 37 | "isDeprecated": false, 38 | "deprecationReason": null 39 | }, 40 | { 41 | "name": "productList", 42 | "description": null, 43 | "args": [], 44 | "type": { 45 | "kind": "OBJECT", 46 | "name": "ProductList", 47 | "ofType": null 48 | }, 49 | "isDeprecated": false, 50 | "deprecationReason": null 51 | }, 52 | { 53 | "name": "cart", 54 | "description": null, 55 | "args": [], 56 | "type": { 57 | "kind": "OBJECT", 58 | "name": "Cart", 59 | "ofType": null 60 | }, 61 | "isDeprecated": false, 62 | "deprecationReason": null 63 | }, 64 | { 65 | "name": "viewer", 66 | "description": null, 67 | "args": [ 68 | { 69 | "name": "id", 70 | "description": null, 71 | "type": { 72 | "kind": "SCALAR", 73 | "name": "ID", 74 | "ofType": null 75 | }, 76 | "defaultValue": null 77 | } 78 | ], 79 | "type": { 80 | "kind": "OBJECT", 81 | "name": "Viewer", 82 | "ofType": null 83 | }, 84 | "isDeprecated": false, 85 | "deprecationReason": null 86 | }, 87 | { 88 | "name": "node", 89 | "description": "Fetches an object given its ID", 90 | "args": [ 91 | { 92 | "name": "id", 93 | "description": "The ID of an object", 94 | "type": { 95 | "kind": "NON_NULL", 96 | "name": null, 97 | "ofType": { 98 | "kind": "SCALAR", 99 | "name": "ID", 100 | "ofType": null 101 | } 102 | }, 103 | "defaultValue": null 104 | } 105 | ], 106 | "type": { 107 | "kind": "INTERFACE", 108 | "name": "Node", 109 | "ofType": null 110 | }, 111 | "isDeprecated": false, 112 | "deprecationReason": null 113 | } 114 | ], 115 | "inputFields": null, 116 | "interfaces": [], 117 | "enumValues": null, 118 | "possibleTypes": null 119 | }, 120 | { 121 | "kind": "SCALAR", 122 | "name": "ID", 123 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 124 | "fields": null, 125 | "inputFields": null, 126 | "interfaces": null, 127 | "enumValues": null, 128 | "possibleTypes": null 129 | }, 130 | { 131 | "kind": "OBJECT", 132 | "name": "Product", 133 | "description": null, 134 | "fields": [ 135 | { 136 | "name": "id", 137 | "description": "The ID of an object", 138 | "args": [], 139 | "type": { 140 | "kind": "NON_NULL", 141 | "name": null, 142 | "ofType": { 143 | "kind": "SCALAR", 144 | "name": "ID", 145 | "ofType": null 146 | } 147 | }, 148 | "isDeprecated": false, 149 | "deprecationReason": null 150 | }, 151 | { 152 | "name": "name", 153 | "description": "商品名称", 154 | "args": [], 155 | "type": { 156 | "kind": "SCALAR", 157 | "name": "String", 158 | "ofType": null 159 | }, 160 | "isDeprecated": false, 161 | "deprecationReason": null 162 | }, 163 | { 164 | "name": "color", 165 | "description": "商品颜色", 166 | "args": [], 167 | "type": { 168 | "kind": "SCALAR", 169 | "name": "String", 170 | "ofType": null 171 | }, 172 | "isDeprecated": false, 173 | "deprecationReason": null 174 | }, 175 | { 176 | "name": "description", 177 | "description": "商品描述", 178 | "args": [], 179 | "type": { 180 | "kind": "SCALAR", 181 | "name": "String", 182 | "ofType": null 183 | }, 184 | "isDeprecated": false, 185 | "deprecationReason": null 186 | }, 187 | { 188 | "name": "price", 189 | "description": "商品价格,保留两位小数", 190 | "args": [], 191 | "type": { 192 | "kind": "SCALAR", 193 | "name": "Float", 194 | "ofType": null 195 | }, 196 | "isDeprecated": false, 197 | "deprecationReason": null 198 | }, 199 | { 200 | "name": "images", 201 | "description": "商品图片,['primary': 主图,'thumbnail':缩略图,'zoom':大图]", 202 | "args": [ 203 | { 204 | "name": "format", 205 | "description": null, 206 | "type": { 207 | "kind": "SCALAR", 208 | "name": "String", 209 | "ofType": null 210 | }, 211 | "defaultValue": "\"any\"" 212 | }, 213 | { 214 | "name": "after", 215 | "description": null, 216 | "type": { 217 | "kind": "SCALAR", 218 | "name": "String", 219 | "ofType": null 220 | }, 221 | "defaultValue": null 222 | }, 223 | { 224 | "name": "first", 225 | "description": null, 226 | "type": { 227 | "kind": "SCALAR", 228 | "name": "Int", 229 | "ofType": null 230 | }, 231 | "defaultValue": null 232 | }, 233 | { 234 | "name": "before", 235 | "description": null, 236 | "type": { 237 | "kind": "SCALAR", 238 | "name": "String", 239 | "ofType": null 240 | }, 241 | "defaultValue": null 242 | }, 243 | { 244 | "name": "last", 245 | "description": null, 246 | "type": { 247 | "kind": "SCALAR", 248 | "name": "Int", 249 | "ofType": null 250 | }, 251 | "defaultValue": null 252 | } 253 | ], 254 | "type": { 255 | "kind": "LIST", 256 | "name": null, 257 | "ofType": { 258 | "kind": "OBJECT", 259 | "name": "Image", 260 | "ofType": null 261 | } 262 | }, 263 | "isDeprecated": false, 264 | "deprecationReason": null 265 | } 266 | ], 267 | "inputFields": null, 268 | "interfaces": [ 269 | { 270 | "kind": "INTERFACE", 271 | "name": "Node", 272 | "ofType": null 273 | } 274 | ], 275 | "enumValues": null, 276 | "possibleTypes": null 277 | }, 278 | { 279 | "kind": "INTERFACE", 280 | "name": "Node", 281 | "description": "An object with an ID", 282 | "fields": [ 283 | { 284 | "name": "id", 285 | "description": "The id of the object.", 286 | "args": [], 287 | "type": { 288 | "kind": "NON_NULL", 289 | "name": null, 290 | "ofType": { 291 | "kind": "SCALAR", 292 | "name": "ID", 293 | "ofType": null 294 | } 295 | }, 296 | "isDeprecated": false, 297 | "deprecationReason": null 298 | } 299 | ], 300 | "inputFields": null, 301 | "interfaces": null, 302 | "enumValues": null, 303 | "possibleTypes": [ 304 | { 305 | "kind": "OBJECT", 306 | "name": "Product", 307 | "ofType": null 308 | }, 309 | { 310 | "kind": "OBJECT", 311 | "name": "ProductList", 312 | "ofType": null 313 | }, 314 | { 315 | "kind": "OBJECT", 316 | "name": "Cart", 317 | "ofType": null 318 | }, 319 | { 320 | "kind": "OBJECT", 321 | "name": "CartEntry", 322 | "ofType": null 323 | }, 324 | { 325 | "kind": "OBJECT", 326 | "name": "Viewer", 327 | "ofType": null 328 | } 329 | ] 330 | }, 331 | { 332 | "kind": "SCALAR", 333 | "name": "String", 334 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 335 | "fields": null, 336 | "inputFields": null, 337 | "interfaces": null, 338 | "enumValues": null, 339 | "possibleTypes": null 340 | }, 341 | { 342 | "kind": "SCALAR", 343 | "name": "Float", 344 | "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", 345 | "fields": null, 346 | "inputFields": null, 347 | "interfaces": null, 348 | "enumValues": null, 349 | "possibleTypes": null 350 | }, 351 | { 352 | "kind": "SCALAR", 353 | "name": "Int", 354 | "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", 355 | "fields": null, 356 | "inputFields": null, 357 | "interfaces": null, 358 | "enumValues": null, 359 | "possibleTypes": null 360 | }, 361 | { 362 | "kind": "OBJECT", 363 | "name": "Image", 364 | "description": "Just image", 365 | "fields": [ 366 | { 367 | "name": "format", 368 | "description": "The format of image.", 369 | "args": [], 370 | "type": { 371 | "kind": "SCALAR", 372 | "name": "String", 373 | "ofType": null 374 | }, 375 | "isDeprecated": false, 376 | "deprecationReason": null 377 | }, 378 | { 379 | "name": "url", 380 | "description": "The url of image.", 381 | "args": [], 382 | "type": { 383 | "kind": "SCALAR", 384 | "name": "String", 385 | "ofType": null 386 | }, 387 | "isDeprecated": false, 388 | "deprecationReason": null 389 | } 390 | ], 391 | "inputFields": null, 392 | "interfaces": [], 393 | "enumValues": null, 394 | "possibleTypes": null 395 | }, 396 | { 397 | "kind": "OBJECT", 398 | "name": "ProductList", 399 | "description": null, 400 | "fields": [ 401 | { 402 | "name": "id", 403 | "description": "The ID of an object", 404 | "args": [], 405 | "type": { 406 | "kind": "NON_NULL", 407 | "name": null, 408 | "ofType": { 409 | "kind": "SCALAR", 410 | "name": "ID", 411 | "ofType": null 412 | } 413 | }, 414 | "isDeprecated": false, 415 | "deprecationReason": null 416 | }, 417 | { 418 | "name": "items", 419 | "description": null, 420 | "args": [ 421 | { 422 | "name": "after", 423 | "description": null, 424 | "type": { 425 | "kind": "SCALAR", 426 | "name": "String", 427 | "ofType": null 428 | }, 429 | "defaultValue": null 430 | }, 431 | { 432 | "name": "first", 433 | "description": null, 434 | "type": { 435 | "kind": "SCALAR", 436 | "name": "Int", 437 | "ofType": null 438 | }, 439 | "defaultValue": null 440 | }, 441 | { 442 | "name": "before", 443 | "description": null, 444 | "type": { 445 | "kind": "SCALAR", 446 | "name": "String", 447 | "ofType": null 448 | }, 449 | "defaultValue": null 450 | }, 451 | { 452 | "name": "last", 453 | "description": null, 454 | "type": { 455 | "kind": "SCALAR", 456 | "name": "Int", 457 | "ofType": null 458 | }, 459 | "defaultValue": null 460 | } 461 | ], 462 | "type": { 463 | "kind": "OBJECT", 464 | "name": "ProductConnection", 465 | "ofType": null 466 | }, 467 | "isDeprecated": false, 468 | "deprecationReason": null 469 | } 470 | ], 471 | "inputFields": null, 472 | "interfaces": [ 473 | { 474 | "kind": "INTERFACE", 475 | "name": "Node", 476 | "ofType": null 477 | } 478 | ], 479 | "enumValues": null, 480 | "possibleTypes": null 481 | }, 482 | { 483 | "kind": "OBJECT", 484 | "name": "ProductConnection", 485 | "description": "A connection to a list of items.", 486 | "fields": [ 487 | { 488 | "name": "pageInfo", 489 | "description": "Information to aid in pagination.", 490 | "args": [], 491 | "type": { 492 | "kind": "NON_NULL", 493 | "name": null, 494 | "ofType": { 495 | "kind": "OBJECT", 496 | "name": "PageInfo", 497 | "ofType": null 498 | } 499 | }, 500 | "isDeprecated": false, 501 | "deprecationReason": null 502 | }, 503 | { 504 | "name": "edges", 505 | "description": "A list of edges.", 506 | "args": [], 507 | "type": { 508 | "kind": "LIST", 509 | "name": null, 510 | "ofType": { 511 | "kind": "OBJECT", 512 | "name": "ProductEdge", 513 | "ofType": null 514 | } 515 | }, 516 | "isDeprecated": false, 517 | "deprecationReason": null 518 | }, 519 | { 520 | "name": "totalNumberOfItems", 521 | "description": null, 522 | "args": [], 523 | "type": { 524 | "kind": "SCALAR", 525 | "name": "Int", 526 | "ofType": null 527 | }, 528 | "isDeprecated": false, 529 | "deprecationReason": null 530 | } 531 | ], 532 | "inputFields": null, 533 | "interfaces": [], 534 | "enumValues": null, 535 | "possibleTypes": null 536 | }, 537 | { 538 | "kind": "OBJECT", 539 | "name": "PageInfo", 540 | "description": "Information about pagination in a connection.", 541 | "fields": [ 542 | { 543 | "name": "hasNextPage", 544 | "description": "When paginating forwards, are there more items?", 545 | "args": [], 546 | "type": { 547 | "kind": "NON_NULL", 548 | "name": null, 549 | "ofType": { 550 | "kind": "SCALAR", 551 | "name": "Boolean", 552 | "ofType": null 553 | } 554 | }, 555 | "isDeprecated": false, 556 | "deprecationReason": null 557 | }, 558 | { 559 | "name": "hasPreviousPage", 560 | "description": "When paginating backwards, are there more items?", 561 | "args": [], 562 | "type": { 563 | "kind": "NON_NULL", 564 | "name": null, 565 | "ofType": { 566 | "kind": "SCALAR", 567 | "name": "Boolean", 568 | "ofType": null 569 | } 570 | }, 571 | "isDeprecated": false, 572 | "deprecationReason": null 573 | }, 574 | { 575 | "name": "startCursor", 576 | "description": "When paginating backwards, the cursor to continue.", 577 | "args": [], 578 | "type": { 579 | "kind": "SCALAR", 580 | "name": "String", 581 | "ofType": null 582 | }, 583 | "isDeprecated": false, 584 | "deprecationReason": null 585 | }, 586 | { 587 | "name": "endCursor", 588 | "description": "When paginating forwards, the cursor to continue.", 589 | "args": [], 590 | "type": { 591 | "kind": "SCALAR", 592 | "name": "String", 593 | "ofType": null 594 | }, 595 | "isDeprecated": false, 596 | "deprecationReason": null 597 | } 598 | ], 599 | "inputFields": null, 600 | "interfaces": [], 601 | "enumValues": null, 602 | "possibleTypes": null 603 | }, 604 | { 605 | "kind": "SCALAR", 606 | "name": "Boolean", 607 | "description": "The `Boolean` scalar type represents `true` or `false`.", 608 | "fields": null, 609 | "inputFields": null, 610 | "interfaces": null, 611 | "enumValues": null, 612 | "possibleTypes": null 613 | }, 614 | { 615 | "kind": "OBJECT", 616 | "name": "ProductEdge", 617 | "description": "An edge in a connection.", 618 | "fields": [ 619 | { 620 | "name": "node", 621 | "description": "The item at the end of the edge", 622 | "args": [], 623 | "type": { 624 | "kind": "OBJECT", 625 | "name": "Product", 626 | "ofType": null 627 | }, 628 | "isDeprecated": false, 629 | "deprecationReason": null 630 | }, 631 | { 632 | "name": "cursor", 633 | "description": "A cursor for use in pagination", 634 | "args": [], 635 | "type": { 636 | "kind": "NON_NULL", 637 | "name": null, 638 | "ofType": { 639 | "kind": "SCALAR", 640 | "name": "String", 641 | "ofType": null 642 | } 643 | }, 644 | "isDeprecated": false, 645 | "deprecationReason": null 646 | } 647 | ], 648 | "inputFields": null, 649 | "interfaces": [], 650 | "enumValues": null, 651 | "possibleTypes": null 652 | }, 653 | { 654 | "kind": "OBJECT", 655 | "name": "Cart", 656 | "description": null, 657 | "fields": [ 658 | { 659 | "name": "id", 660 | "description": "The ID of an object", 661 | "args": [], 662 | "type": { 663 | "kind": "NON_NULL", 664 | "name": null, 665 | "ofType": { 666 | "kind": "SCALAR", 667 | "name": "ID", 668 | "ofType": null 669 | } 670 | }, 671 | "isDeprecated": false, 672 | "deprecationReason": null 673 | }, 674 | { 675 | "name": "entries", 676 | "description": null, 677 | "args": [ 678 | { 679 | "name": "after", 680 | "description": null, 681 | "type": { 682 | "kind": "SCALAR", 683 | "name": "String", 684 | "ofType": null 685 | }, 686 | "defaultValue": null 687 | }, 688 | { 689 | "name": "first", 690 | "description": null, 691 | "type": { 692 | "kind": "SCALAR", 693 | "name": "Int", 694 | "ofType": null 695 | }, 696 | "defaultValue": null 697 | }, 698 | { 699 | "name": "before", 700 | "description": null, 701 | "type": { 702 | "kind": "SCALAR", 703 | "name": "String", 704 | "ofType": null 705 | }, 706 | "defaultValue": null 707 | }, 708 | { 709 | "name": "last", 710 | "description": null, 711 | "type": { 712 | "kind": "SCALAR", 713 | "name": "Int", 714 | "ofType": null 715 | }, 716 | "defaultValue": null 717 | } 718 | ], 719 | "type": { 720 | "kind": "OBJECT", 721 | "name": "CartEntryConnection", 722 | "ofType": null 723 | }, 724 | "isDeprecated": false, 725 | "deprecationReason": null 726 | }, 727 | { 728 | "name": "totalNumberOfItems", 729 | "description": null, 730 | "args": [], 731 | "type": { 732 | "kind": "SCALAR", 733 | "name": "Int", 734 | "ofType": null 735 | }, 736 | "isDeprecated": false, 737 | "deprecationReason": null 738 | }, 739 | { 740 | "name": "totalPriceOfItems", 741 | "description": null, 742 | "args": [], 743 | "type": { 744 | "kind": "SCALAR", 745 | "name": "Float", 746 | "ofType": null 747 | }, 748 | "isDeprecated": false, 749 | "deprecationReason": null 750 | } 751 | ], 752 | "inputFields": null, 753 | "interfaces": [ 754 | { 755 | "kind": "INTERFACE", 756 | "name": "Node", 757 | "ofType": null 758 | } 759 | ], 760 | "enumValues": null, 761 | "possibleTypes": null 762 | }, 763 | { 764 | "kind": "OBJECT", 765 | "name": "CartEntryConnection", 766 | "description": "A connection to a list of items.", 767 | "fields": [ 768 | { 769 | "name": "pageInfo", 770 | "description": "Information to aid in pagination.", 771 | "args": [], 772 | "type": { 773 | "kind": "NON_NULL", 774 | "name": null, 775 | "ofType": { 776 | "kind": "OBJECT", 777 | "name": "PageInfo", 778 | "ofType": null 779 | } 780 | }, 781 | "isDeprecated": false, 782 | "deprecationReason": null 783 | }, 784 | { 785 | "name": "edges", 786 | "description": "A list of edges.", 787 | "args": [], 788 | "type": { 789 | "kind": "LIST", 790 | "name": null, 791 | "ofType": { 792 | "kind": "OBJECT", 793 | "name": "CartEntryEdge", 794 | "ofType": null 795 | } 796 | }, 797 | "isDeprecated": false, 798 | "deprecationReason": null 799 | } 800 | ], 801 | "inputFields": null, 802 | "interfaces": [], 803 | "enumValues": null, 804 | "possibleTypes": null 805 | }, 806 | { 807 | "kind": "OBJECT", 808 | "name": "CartEntryEdge", 809 | "description": "An edge in a connection.", 810 | "fields": [ 811 | { 812 | "name": "node", 813 | "description": "The item at the end of the edge", 814 | "args": [], 815 | "type": { 816 | "kind": "OBJECT", 817 | "name": "CartEntry", 818 | "ofType": null 819 | }, 820 | "isDeprecated": false, 821 | "deprecationReason": null 822 | }, 823 | { 824 | "name": "cursor", 825 | "description": "A cursor for use in pagination", 826 | "args": [], 827 | "type": { 828 | "kind": "NON_NULL", 829 | "name": null, 830 | "ofType": { 831 | "kind": "SCALAR", 832 | "name": "String", 833 | "ofType": null 834 | } 835 | }, 836 | "isDeprecated": false, 837 | "deprecationReason": null 838 | } 839 | ], 840 | "inputFields": null, 841 | "interfaces": [], 842 | "enumValues": null, 843 | "possibleTypes": null 844 | }, 845 | { 846 | "kind": "OBJECT", 847 | "name": "CartEntry", 848 | "description": null, 849 | "fields": [ 850 | { 851 | "name": "id", 852 | "description": "The ID of an object", 853 | "args": [], 854 | "type": { 855 | "kind": "NON_NULL", 856 | "name": null, 857 | "ofType": { 858 | "kind": "SCALAR", 859 | "name": "ID", 860 | "ofType": null 861 | } 862 | }, 863 | "isDeprecated": false, 864 | "deprecationReason": null 865 | }, 866 | { 867 | "name": "product", 868 | "description": null, 869 | "args": [], 870 | "type": { 871 | "kind": "OBJECT", 872 | "name": "Product", 873 | "ofType": null 874 | }, 875 | "isDeprecated": false, 876 | "deprecationReason": null 877 | }, 878 | { 879 | "name": "quantity", 880 | "description": null, 881 | "args": [], 882 | "type": { 883 | "kind": "SCALAR", 884 | "name": "Float", 885 | "ofType": null 886 | }, 887 | "isDeprecated": false, 888 | "deprecationReason": null 889 | }, 890 | { 891 | "name": "price", 892 | "description": null, 893 | "args": [], 894 | "type": { 895 | "kind": "SCALAR", 896 | "name": "Float", 897 | "ofType": null 898 | }, 899 | "isDeprecated": false, 900 | "deprecationReason": null 901 | }, 902 | { 903 | "name": "totalPrice", 904 | "description": null, 905 | "args": [], 906 | "type": { 907 | "kind": "SCALAR", 908 | "name": "Float", 909 | "ofType": null 910 | }, 911 | "isDeprecated": false, 912 | "deprecationReason": null 913 | } 914 | ], 915 | "inputFields": null, 916 | "interfaces": [ 917 | { 918 | "kind": "INTERFACE", 919 | "name": "Node", 920 | "ofType": null 921 | } 922 | ], 923 | "enumValues": null, 924 | "possibleTypes": null 925 | }, 926 | { 927 | "kind": "OBJECT", 928 | "name": "Viewer", 929 | "description": null, 930 | "fields": [ 931 | { 932 | "name": "id", 933 | "description": "The ID of an object", 934 | "args": [], 935 | "type": { 936 | "kind": "NON_NULL", 937 | "name": null, 938 | "ofType": { 939 | "kind": "SCALAR", 940 | "name": "ID", 941 | "ofType": null 942 | } 943 | }, 944 | "isDeprecated": false, 945 | "deprecationReason": null 946 | }, 947 | { 948 | "name": "name", 949 | "description": null, 950 | "args": [], 951 | "type": { 952 | "kind": "SCALAR", 953 | "name": "String", 954 | "ofType": null 955 | }, 956 | "isDeprecated": false, 957 | "deprecationReason": null 958 | }, 959 | { 960 | "name": "cart", 961 | "description": null, 962 | "args": [], 963 | "type": { 964 | "kind": "OBJECT", 965 | "name": "Cart", 966 | "ofType": null 967 | }, 968 | "isDeprecated": false, 969 | "deprecationReason": null 970 | }, 971 | { 972 | "name": "products", 973 | "description": null, 974 | "args": [ 975 | { 976 | "name": "after", 977 | "description": null, 978 | "type": { 979 | "kind": "SCALAR", 980 | "name": "String", 981 | "ofType": null 982 | }, 983 | "defaultValue": null 984 | }, 985 | { 986 | "name": "first", 987 | "description": null, 988 | "type": { 989 | "kind": "SCALAR", 990 | "name": "Int", 991 | "ofType": null 992 | }, 993 | "defaultValue": null 994 | }, 995 | { 996 | "name": "before", 997 | "description": null, 998 | "type": { 999 | "kind": "SCALAR", 1000 | "name": "String", 1001 | "ofType": null 1002 | }, 1003 | "defaultValue": null 1004 | }, 1005 | { 1006 | "name": "last", 1007 | "description": null, 1008 | "type": { 1009 | "kind": "SCALAR", 1010 | "name": "Int", 1011 | "ofType": null 1012 | }, 1013 | "defaultValue": null 1014 | } 1015 | ], 1016 | "type": { 1017 | "kind": "OBJECT", 1018 | "name": "ProductConnection", 1019 | "ofType": null 1020 | }, 1021 | "isDeprecated": false, 1022 | "deprecationReason": null 1023 | } 1024 | ], 1025 | "inputFields": null, 1026 | "interfaces": [ 1027 | { 1028 | "kind": "INTERFACE", 1029 | "name": "Node", 1030 | "ofType": null 1031 | } 1032 | ], 1033 | "enumValues": null, 1034 | "possibleTypes": null 1035 | }, 1036 | { 1037 | "kind": "OBJECT", 1038 | "name": "Mutation", 1039 | "description": null, 1040 | "fields": [ 1041 | { 1042 | "name": "addToCart", 1043 | "description": null, 1044 | "args": [ 1045 | { 1046 | "name": "input", 1047 | "description": null, 1048 | "type": { 1049 | "kind": "NON_NULL", 1050 | "name": null, 1051 | "ofType": { 1052 | "kind": "INPUT_OBJECT", 1053 | "name": "AddToCartInput", 1054 | "ofType": null 1055 | } 1056 | }, 1057 | "defaultValue": null 1058 | } 1059 | ], 1060 | "type": { 1061 | "kind": "OBJECT", 1062 | "name": "AddToCartPayload", 1063 | "ofType": null 1064 | }, 1065 | "isDeprecated": false, 1066 | "deprecationReason": null 1067 | }, 1068 | { 1069 | "name": "removeFromCart", 1070 | "description": null, 1071 | "args": [ 1072 | { 1073 | "name": "input", 1074 | "description": null, 1075 | "type": { 1076 | "kind": "NON_NULL", 1077 | "name": null, 1078 | "ofType": { 1079 | "kind": "INPUT_OBJECT", 1080 | "name": "RemoveFromCartInput", 1081 | "ofType": null 1082 | } 1083 | }, 1084 | "defaultValue": null 1085 | } 1086 | ], 1087 | "type": { 1088 | "kind": "OBJECT", 1089 | "name": "RemoveFromCartPayload", 1090 | "ofType": null 1091 | }, 1092 | "isDeprecated": false, 1093 | "deprecationReason": null 1094 | } 1095 | ], 1096 | "inputFields": null, 1097 | "interfaces": [], 1098 | "enumValues": null, 1099 | "possibleTypes": null 1100 | }, 1101 | { 1102 | "kind": "INPUT_OBJECT", 1103 | "name": "AddToCartInput", 1104 | "description": null, 1105 | "fields": null, 1106 | "inputFields": [ 1107 | { 1108 | "name": "id", 1109 | "description": null, 1110 | "type": { 1111 | "kind": "NON_NULL", 1112 | "name": null, 1113 | "ofType": { 1114 | "kind": "SCALAR", 1115 | "name": "ID", 1116 | "ofType": null 1117 | } 1118 | }, 1119 | "defaultValue": null 1120 | }, 1121 | { 1122 | "name": "quantity", 1123 | "description": null, 1124 | "type": { 1125 | "kind": "SCALAR", 1126 | "name": "Int", 1127 | "ofType": null 1128 | }, 1129 | "defaultValue": null 1130 | }, 1131 | { 1132 | "name": "clientMutationId", 1133 | "description": null, 1134 | "type": { 1135 | "kind": "SCALAR", 1136 | "name": "String", 1137 | "ofType": null 1138 | }, 1139 | "defaultValue": null 1140 | } 1141 | ], 1142 | "interfaces": null, 1143 | "enumValues": null, 1144 | "possibleTypes": null 1145 | }, 1146 | { 1147 | "kind": "OBJECT", 1148 | "name": "AddToCartPayload", 1149 | "description": null, 1150 | "fields": [ 1151 | { 1152 | "name": "cartEntryEdge", 1153 | "description": null, 1154 | "args": [], 1155 | "type": { 1156 | "kind": "OBJECT", 1157 | "name": "CartEntryEdge", 1158 | "ofType": null 1159 | }, 1160 | "isDeprecated": false, 1161 | "deprecationReason": null 1162 | }, 1163 | { 1164 | "name": "cartEntry", 1165 | "description": null, 1166 | "args": [], 1167 | "type": { 1168 | "kind": "OBJECT", 1169 | "name": "CartEntry", 1170 | "ofType": null 1171 | }, 1172 | "isDeprecated": false, 1173 | "deprecationReason": null 1174 | }, 1175 | { 1176 | "name": "cart", 1177 | "description": null, 1178 | "args": [], 1179 | "type": { 1180 | "kind": "OBJECT", 1181 | "name": "Cart", 1182 | "ofType": null 1183 | }, 1184 | "isDeprecated": false, 1185 | "deprecationReason": null 1186 | }, 1187 | { 1188 | "name": "clientMutationId", 1189 | "description": null, 1190 | "args": [], 1191 | "type": { 1192 | "kind": "SCALAR", 1193 | "name": "String", 1194 | "ofType": null 1195 | }, 1196 | "isDeprecated": false, 1197 | "deprecationReason": null 1198 | } 1199 | ], 1200 | "inputFields": null, 1201 | "interfaces": [], 1202 | "enumValues": null, 1203 | "possibleTypes": null 1204 | }, 1205 | { 1206 | "kind": "INPUT_OBJECT", 1207 | "name": "RemoveFromCartInput", 1208 | "description": null, 1209 | "fields": null, 1210 | "inputFields": [ 1211 | { 1212 | "name": "id", 1213 | "description": null, 1214 | "type": { 1215 | "kind": "NON_NULL", 1216 | "name": null, 1217 | "ofType": { 1218 | "kind": "SCALAR", 1219 | "name": "ID", 1220 | "ofType": null 1221 | } 1222 | }, 1223 | "defaultValue": null 1224 | }, 1225 | { 1226 | "name": "clientMutationId", 1227 | "description": null, 1228 | "type": { 1229 | "kind": "SCALAR", 1230 | "name": "String", 1231 | "ofType": null 1232 | }, 1233 | "defaultValue": null 1234 | } 1235 | ], 1236 | "interfaces": null, 1237 | "enumValues": null, 1238 | "possibleTypes": null 1239 | }, 1240 | { 1241 | "kind": "OBJECT", 1242 | "name": "RemoveFromCartPayload", 1243 | "description": null, 1244 | "fields": [ 1245 | { 1246 | "name": "deletedCartEntryId", 1247 | "description": null, 1248 | "args": [], 1249 | "type": { 1250 | "kind": "SCALAR", 1251 | "name": "ID", 1252 | "ofType": null 1253 | }, 1254 | "isDeprecated": false, 1255 | "deprecationReason": null 1256 | }, 1257 | { 1258 | "name": "cart", 1259 | "description": null, 1260 | "args": [], 1261 | "type": { 1262 | "kind": "OBJECT", 1263 | "name": "Cart", 1264 | "ofType": null 1265 | }, 1266 | "isDeprecated": false, 1267 | "deprecationReason": null 1268 | }, 1269 | { 1270 | "name": "clientMutationId", 1271 | "description": null, 1272 | "args": [], 1273 | "type": { 1274 | "kind": "SCALAR", 1275 | "name": "String", 1276 | "ofType": null 1277 | }, 1278 | "isDeprecated": false, 1279 | "deprecationReason": null 1280 | } 1281 | ], 1282 | "inputFields": null, 1283 | "interfaces": [], 1284 | "enumValues": null, 1285 | "possibleTypes": null 1286 | }, 1287 | { 1288 | "kind": "OBJECT", 1289 | "name": "__Schema", 1290 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 1291 | "fields": [ 1292 | { 1293 | "name": "types", 1294 | "description": "A list of all types supported by this server.", 1295 | "args": [], 1296 | "type": { 1297 | "kind": "NON_NULL", 1298 | "name": null, 1299 | "ofType": { 1300 | "kind": "LIST", 1301 | "name": null, 1302 | "ofType": { 1303 | "kind": "NON_NULL", 1304 | "name": null, 1305 | "ofType": { 1306 | "kind": "OBJECT", 1307 | "name": "__Type", 1308 | "ofType": null 1309 | } 1310 | } 1311 | } 1312 | }, 1313 | "isDeprecated": false, 1314 | "deprecationReason": null 1315 | }, 1316 | { 1317 | "name": "queryType", 1318 | "description": "The type that query operations will be rooted at.", 1319 | "args": [], 1320 | "type": { 1321 | "kind": "NON_NULL", 1322 | "name": null, 1323 | "ofType": { 1324 | "kind": "OBJECT", 1325 | "name": "__Type", 1326 | "ofType": null 1327 | } 1328 | }, 1329 | "isDeprecated": false, 1330 | "deprecationReason": null 1331 | }, 1332 | { 1333 | "name": "mutationType", 1334 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 1335 | "args": [], 1336 | "type": { 1337 | "kind": "OBJECT", 1338 | "name": "__Type", 1339 | "ofType": null 1340 | }, 1341 | "isDeprecated": false, 1342 | "deprecationReason": null 1343 | }, 1344 | { 1345 | "name": "subscriptionType", 1346 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 1347 | "args": [], 1348 | "type": { 1349 | "kind": "OBJECT", 1350 | "name": "__Type", 1351 | "ofType": null 1352 | }, 1353 | "isDeprecated": false, 1354 | "deprecationReason": null 1355 | }, 1356 | { 1357 | "name": "directives", 1358 | "description": "A list of all directives supported by this server.", 1359 | "args": [], 1360 | "type": { 1361 | "kind": "NON_NULL", 1362 | "name": null, 1363 | "ofType": { 1364 | "kind": "LIST", 1365 | "name": null, 1366 | "ofType": { 1367 | "kind": "NON_NULL", 1368 | "name": null, 1369 | "ofType": { 1370 | "kind": "OBJECT", 1371 | "name": "__Directive", 1372 | "ofType": null 1373 | } 1374 | } 1375 | } 1376 | }, 1377 | "isDeprecated": false, 1378 | "deprecationReason": null 1379 | } 1380 | ], 1381 | "inputFields": null, 1382 | "interfaces": [], 1383 | "enumValues": null, 1384 | "possibleTypes": null 1385 | }, 1386 | { 1387 | "kind": "OBJECT", 1388 | "name": "__Type", 1389 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 1390 | "fields": [ 1391 | { 1392 | "name": "kind", 1393 | "description": null, 1394 | "args": [], 1395 | "type": { 1396 | "kind": "NON_NULL", 1397 | "name": null, 1398 | "ofType": { 1399 | "kind": "ENUM", 1400 | "name": "__TypeKind", 1401 | "ofType": null 1402 | } 1403 | }, 1404 | "isDeprecated": false, 1405 | "deprecationReason": null 1406 | }, 1407 | { 1408 | "name": "name", 1409 | "description": null, 1410 | "args": [], 1411 | "type": { 1412 | "kind": "SCALAR", 1413 | "name": "String", 1414 | "ofType": null 1415 | }, 1416 | "isDeprecated": false, 1417 | "deprecationReason": null 1418 | }, 1419 | { 1420 | "name": "description", 1421 | "description": null, 1422 | "args": [], 1423 | "type": { 1424 | "kind": "SCALAR", 1425 | "name": "String", 1426 | "ofType": null 1427 | }, 1428 | "isDeprecated": false, 1429 | "deprecationReason": null 1430 | }, 1431 | { 1432 | "name": "fields", 1433 | "description": null, 1434 | "args": [ 1435 | { 1436 | "name": "includeDeprecated", 1437 | "description": null, 1438 | "type": { 1439 | "kind": "SCALAR", 1440 | "name": "Boolean", 1441 | "ofType": null 1442 | }, 1443 | "defaultValue": "false" 1444 | } 1445 | ], 1446 | "type": { 1447 | "kind": "LIST", 1448 | "name": null, 1449 | "ofType": { 1450 | "kind": "NON_NULL", 1451 | "name": null, 1452 | "ofType": { 1453 | "kind": "OBJECT", 1454 | "name": "__Field", 1455 | "ofType": null 1456 | } 1457 | } 1458 | }, 1459 | "isDeprecated": false, 1460 | "deprecationReason": null 1461 | }, 1462 | { 1463 | "name": "interfaces", 1464 | "description": null, 1465 | "args": [], 1466 | "type": { 1467 | "kind": "LIST", 1468 | "name": null, 1469 | "ofType": { 1470 | "kind": "NON_NULL", 1471 | "name": null, 1472 | "ofType": { 1473 | "kind": "OBJECT", 1474 | "name": "__Type", 1475 | "ofType": null 1476 | } 1477 | } 1478 | }, 1479 | "isDeprecated": false, 1480 | "deprecationReason": null 1481 | }, 1482 | { 1483 | "name": "possibleTypes", 1484 | "description": null, 1485 | "args": [], 1486 | "type": { 1487 | "kind": "LIST", 1488 | "name": null, 1489 | "ofType": { 1490 | "kind": "NON_NULL", 1491 | "name": null, 1492 | "ofType": { 1493 | "kind": "OBJECT", 1494 | "name": "__Type", 1495 | "ofType": null 1496 | } 1497 | } 1498 | }, 1499 | "isDeprecated": false, 1500 | "deprecationReason": null 1501 | }, 1502 | { 1503 | "name": "enumValues", 1504 | "description": null, 1505 | "args": [ 1506 | { 1507 | "name": "includeDeprecated", 1508 | "description": null, 1509 | "type": { 1510 | "kind": "SCALAR", 1511 | "name": "Boolean", 1512 | "ofType": null 1513 | }, 1514 | "defaultValue": "false" 1515 | } 1516 | ], 1517 | "type": { 1518 | "kind": "LIST", 1519 | "name": null, 1520 | "ofType": { 1521 | "kind": "NON_NULL", 1522 | "name": null, 1523 | "ofType": { 1524 | "kind": "OBJECT", 1525 | "name": "__EnumValue", 1526 | "ofType": null 1527 | } 1528 | } 1529 | }, 1530 | "isDeprecated": false, 1531 | "deprecationReason": null 1532 | }, 1533 | { 1534 | "name": "inputFields", 1535 | "description": null, 1536 | "args": [], 1537 | "type": { 1538 | "kind": "LIST", 1539 | "name": null, 1540 | "ofType": { 1541 | "kind": "NON_NULL", 1542 | "name": null, 1543 | "ofType": { 1544 | "kind": "OBJECT", 1545 | "name": "__InputValue", 1546 | "ofType": null 1547 | } 1548 | } 1549 | }, 1550 | "isDeprecated": false, 1551 | "deprecationReason": null 1552 | }, 1553 | { 1554 | "name": "ofType", 1555 | "description": null, 1556 | "args": [], 1557 | "type": { 1558 | "kind": "OBJECT", 1559 | "name": "__Type", 1560 | "ofType": null 1561 | }, 1562 | "isDeprecated": false, 1563 | "deprecationReason": null 1564 | } 1565 | ], 1566 | "inputFields": null, 1567 | "interfaces": [], 1568 | "enumValues": null, 1569 | "possibleTypes": null 1570 | }, 1571 | { 1572 | "kind": "ENUM", 1573 | "name": "__TypeKind", 1574 | "description": "An enum describing what kind of type a given `__Type` is.", 1575 | "fields": null, 1576 | "inputFields": null, 1577 | "interfaces": null, 1578 | "enumValues": [ 1579 | { 1580 | "name": "SCALAR", 1581 | "description": "Indicates this type is a scalar.", 1582 | "isDeprecated": false, 1583 | "deprecationReason": null 1584 | }, 1585 | { 1586 | "name": "OBJECT", 1587 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 1588 | "isDeprecated": false, 1589 | "deprecationReason": null 1590 | }, 1591 | { 1592 | "name": "INTERFACE", 1593 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 1594 | "isDeprecated": false, 1595 | "deprecationReason": null 1596 | }, 1597 | { 1598 | "name": "UNION", 1599 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 1600 | "isDeprecated": false, 1601 | "deprecationReason": null 1602 | }, 1603 | { 1604 | "name": "ENUM", 1605 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 1606 | "isDeprecated": false, 1607 | "deprecationReason": null 1608 | }, 1609 | { 1610 | "name": "INPUT_OBJECT", 1611 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 1612 | "isDeprecated": false, 1613 | "deprecationReason": null 1614 | }, 1615 | { 1616 | "name": "LIST", 1617 | "description": "Indicates this type is a list. `ofType` is a valid field.", 1618 | "isDeprecated": false, 1619 | "deprecationReason": null 1620 | }, 1621 | { 1622 | "name": "NON_NULL", 1623 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 1624 | "isDeprecated": false, 1625 | "deprecationReason": null 1626 | } 1627 | ], 1628 | "possibleTypes": null 1629 | }, 1630 | { 1631 | "kind": "OBJECT", 1632 | "name": "__Field", 1633 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 1634 | "fields": [ 1635 | { 1636 | "name": "name", 1637 | "description": null, 1638 | "args": [], 1639 | "type": { 1640 | "kind": "NON_NULL", 1641 | "name": null, 1642 | "ofType": { 1643 | "kind": "SCALAR", 1644 | "name": "String", 1645 | "ofType": null 1646 | } 1647 | }, 1648 | "isDeprecated": false, 1649 | "deprecationReason": null 1650 | }, 1651 | { 1652 | "name": "description", 1653 | "description": null, 1654 | "args": [], 1655 | "type": { 1656 | "kind": "SCALAR", 1657 | "name": "String", 1658 | "ofType": null 1659 | }, 1660 | "isDeprecated": false, 1661 | "deprecationReason": null 1662 | }, 1663 | { 1664 | "name": "args", 1665 | "description": null, 1666 | "args": [], 1667 | "type": { 1668 | "kind": "NON_NULL", 1669 | "name": null, 1670 | "ofType": { 1671 | "kind": "LIST", 1672 | "name": null, 1673 | "ofType": { 1674 | "kind": "NON_NULL", 1675 | "name": null, 1676 | "ofType": { 1677 | "kind": "OBJECT", 1678 | "name": "__InputValue", 1679 | "ofType": null 1680 | } 1681 | } 1682 | } 1683 | }, 1684 | "isDeprecated": false, 1685 | "deprecationReason": null 1686 | }, 1687 | { 1688 | "name": "type", 1689 | "description": null, 1690 | "args": [], 1691 | "type": { 1692 | "kind": "NON_NULL", 1693 | "name": null, 1694 | "ofType": { 1695 | "kind": "OBJECT", 1696 | "name": "__Type", 1697 | "ofType": null 1698 | } 1699 | }, 1700 | "isDeprecated": false, 1701 | "deprecationReason": null 1702 | }, 1703 | { 1704 | "name": "isDeprecated", 1705 | "description": null, 1706 | "args": [], 1707 | "type": { 1708 | "kind": "NON_NULL", 1709 | "name": null, 1710 | "ofType": { 1711 | "kind": "SCALAR", 1712 | "name": "Boolean", 1713 | "ofType": null 1714 | } 1715 | }, 1716 | "isDeprecated": false, 1717 | "deprecationReason": null 1718 | }, 1719 | { 1720 | "name": "deprecationReason", 1721 | "description": null, 1722 | "args": [], 1723 | "type": { 1724 | "kind": "SCALAR", 1725 | "name": "String", 1726 | "ofType": null 1727 | }, 1728 | "isDeprecated": false, 1729 | "deprecationReason": null 1730 | } 1731 | ], 1732 | "inputFields": null, 1733 | "interfaces": [], 1734 | "enumValues": null, 1735 | "possibleTypes": null 1736 | }, 1737 | { 1738 | "kind": "OBJECT", 1739 | "name": "__InputValue", 1740 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 1741 | "fields": [ 1742 | { 1743 | "name": "name", 1744 | "description": null, 1745 | "args": [], 1746 | "type": { 1747 | "kind": "NON_NULL", 1748 | "name": null, 1749 | "ofType": { 1750 | "kind": "SCALAR", 1751 | "name": "String", 1752 | "ofType": null 1753 | } 1754 | }, 1755 | "isDeprecated": false, 1756 | "deprecationReason": null 1757 | }, 1758 | { 1759 | "name": "description", 1760 | "description": null, 1761 | "args": [], 1762 | "type": { 1763 | "kind": "SCALAR", 1764 | "name": "String", 1765 | "ofType": null 1766 | }, 1767 | "isDeprecated": false, 1768 | "deprecationReason": null 1769 | }, 1770 | { 1771 | "name": "type", 1772 | "description": null, 1773 | "args": [], 1774 | "type": { 1775 | "kind": "NON_NULL", 1776 | "name": null, 1777 | "ofType": { 1778 | "kind": "OBJECT", 1779 | "name": "__Type", 1780 | "ofType": null 1781 | } 1782 | }, 1783 | "isDeprecated": false, 1784 | "deprecationReason": null 1785 | }, 1786 | { 1787 | "name": "defaultValue", 1788 | "description": "A GraphQL-formatted string representing the default value for this input value.", 1789 | "args": [], 1790 | "type": { 1791 | "kind": "SCALAR", 1792 | "name": "String", 1793 | "ofType": null 1794 | }, 1795 | "isDeprecated": false, 1796 | "deprecationReason": null 1797 | } 1798 | ], 1799 | "inputFields": null, 1800 | "interfaces": [], 1801 | "enumValues": null, 1802 | "possibleTypes": null 1803 | }, 1804 | { 1805 | "kind": "OBJECT", 1806 | "name": "__EnumValue", 1807 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 1808 | "fields": [ 1809 | { 1810 | "name": "name", 1811 | "description": null, 1812 | "args": [], 1813 | "type": { 1814 | "kind": "NON_NULL", 1815 | "name": null, 1816 | "ofType": { 1817 | "kind": "SCALAR", 1818 | "name": "String", 1819 | "ofType": null 1820 | } 1821 | }, 1822 | "isDeprecated": false, 1823 | "deprecationReason": null 1824 | }, 1825 | { 1826 | "name": "description", 1827 | "description": null, 1828 | "args": [], 1829 | "type": { 1830 | "kind": "SCALAR", 1831 | "name": "String", 1832 | "ofType": null 1833 | }, 1834 | "isDeprecated": false, 1835 | "deprecationReason": null 1836 | }, 1837 | { 1838 | "name": "isDeprecated", 1839 | "description": null, 1840 | "args": [], 1841 | "type": { 1842 | "kind": "NON_NULL", 1843 | "name": null, 1844 | "ofType": { 1845 | "kind": "SCALAR", 1846 | "name": "Boolean", 1847 | "ofType": null 1848 | } 1849 | }, 1850 | "isDeprecated": false, 1851 | "deprecationReason": null 1852 | }, 1853 | { 1854 | "name": "deprecationReason", 1855 | "description": null, 1856 | "args": [], 1857 | "type": { 1858 | "kind": "SCALAR", 1859 | "name": "String", 1860 | "ofType": null 1861 | }, 1862 | "isDeprecated": false, 1863 | "deprecationReason": null 1864 | } 1865 | ], 1866 | "inputFields": null, 1867 | "interfaces": [], 1868 | "enumValues": null, 1869 | "possibleTypes": null 1870 | }, 1871 | { 1872 | "kind": "OBJECT", 1873 | "name": "__Directive", 1874 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 1875 | "fields": [ 1876 | { 1877 | "name": "name", 1878 | "description": null, 1879 | "args": [], 1880 | "type": { 1881 | "kind": "NON_NULL", 1882 | "name": null, 1883 | "ofType": { 1884 | "kind": "SCALAR", 1885 | "name": "String", 1886 | "ofType": null 1887 | } 1888 | }, 1889 | "isDeprecated": false, 1890 | "deprecationReason": null 1891 | }, 1892 | { 1893 | "name": "description", 1894 | "description": null, 1895 | "args": [], 1896 | "type": { 1897 | "kind": "SCALAR", 1898 | "name": "String", 1899 | "ofType": null 1900 | }, 1901 | "isDeprecated": false, 1902 | "deprecationReason": null 1903 | }, 1904 | { 1905 | "name": "locations", 1906 | "description": null, 1907 | "args": [], 1908 | "type": { 1909 | "kind": "NON_NULL", 1910 | "name": null, 1911 | "ofType": { 1912 | "kind": "LIST", 1913 | "name": null, 1914 | "ofType": { 1915 | "kind": "NON_NULL", 1916 | "name": null, 1917 | "ofType": { 1918 | "kind": "ENUM", 1919 | "name": "__DirectiveLocation", 1920 | "ofType": null 1921 | } 1922 | } 1923 | } 1924 | }, 1925 | "isDeprecated": false, 1926 | "deprecationReason": null 1927 | }, 1928 | { 1929 | "name": "args", 1930 | "description": null, 1931 | "args": [], 1932 | "type": { 1933 | "kind": "NON_NULL", 1934 | "name": null, 1935 | "ofType": { 1936 | "kind": "LIST", 1937 | "name": null, 1938 | "ofType": { 1939 | "kind": "NON_NULL", 1940 | "name": null, 1941 | "ofType": { 1942 | "kind": "OBJECT", 1943 | "name": "__InputValue", 1944 | "ofType": null 1945 | } 1946 | } 1947 | } 1948 | }, 1949 | "isDeprecated": false, 1950 | "deprecationReason": null 1951 | }, 1952 | { 1953 | "name": "onOperation", 1954 | "description": null, 1955 | "args": [], 1956 | "type": { 1957 | "kind": "NON_NULL", 1958 | "name": null, 1959 | "ofType": { 1960 | "kind": "SCALAR", 1961 | "name": "Boolean", 1962 | "ofType": null 1963 | } 1964 | }, 1965 | "isDeprecated": true, 1966 | "deprecationReason": "Use `locations`." 1967 | }, 1968 | { 1969 | "name": "onFragment", 1970 | "description": null, 1971 | "args": [], 1972 | "type": { 1973 | "kind": "NON_NULL", 1974 | "name": null, 1975 | "ofType": { 1976 | "kind": "SCALAR", 1977 | "name": "Boolean", 1978 | "ofType": null 1979 | } 1980 | }, 1981 | "isDeprecated": true, 1982 | "deprecationReason": "Use `locations`." 1983 | }, 1984 | { 1985 | "name": "onField", 1986 | "description": null, 1987 | "args": [], 1988 | "type": { 1989 | "kind": "NON_NULL", 1990 | "name": null, 1991 | "ofType": { 1992 | "kind": "SCALAR", 1993 | "name": "Boolean", 1994 | "ofType": null 1995 | } 1996 | }, 1997 | "isDeprecated": true, 1998 | "deprecationReason": "Use `locations`." 1999 | } 2000 | ], 2001 | "inputFields": null, 2002 | "interfaces": [], 2003 | "enumValues": null, 2004 | "possibleTypes": null 2005 | }, 2006 | { 2007 | "kind": "ENUM", 2008 | "name": "__DirectiveLocation", 2009 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 2010 | "fields": null, 2011 | "inputFields": null, 2012 | "interfaces": null, 2013 | "enumValues": [ 2014 | { 2015 | "name": "QUERY", 2016 | "description": "Location adjacent to a query operation.", 2017 | "isDeprecated": false, 2018 | "deprecationReason": null 2019 | }, 2020 | { 2021 | "name": "MUTATION", 2022 | "description": "Location adjacent to a mutation operation.", 2023 | "isDeprecated": false, 2024 | "deprecationReason": null 2025 | }, 2026 | { 2027 | "name": "SUBSCRIPTION", 2028 | "description": "Location adjacent to a subscription operation.", 2029 | "isDeprecated": false, 2030 | "deprecationReason": null 2031 | }, 2032 | { 2033 | "name": "FIELD", 2034 | "description": "Location adjacent to a field.", 2035 | "isDeprecated": false, 2036 | "deprecationReason": null 2037 | }, 2038 | { 2039 | "name": "FRAGMENT_DEFINITION", 2040 | "description": "Location adjacent to a fragment definition.", 2041 | "isDeprecated": false, 2042 | "deprecationReason": null 2043 | }, 2044 | { 2045 | "name": "FRAGMENT_SPREAD", 2046 | "description": "Location adjacent to a fragment spread.", 2047 | "isDeprecated": false, 2048 | "deprecationReason": null 2049 | }, 2050 | { 2051 | "name": "INLINE_FRAGMENT", 2052 | "description": "Location adjacent to an inline fragment.", 2053 | "isDeprecated": false, 2054 | "deprecationReason": null 2055 | }, 2056 | { 2057 | "name": "SCHEMA", 2058 | "description": "Location adjacent to a schema definition.", 2059 | "isDeprecated": false, 2060 | "deprecationReason": null 2061 | }, 2062 | { 2063 | "name": "SCALAR", 2064 | "description": "Location adjacent to a scalar definition.", 2065 | "isDeprecated": false, 2066 | "deprecationReason": null 2067 | }, 2068 | { 2069 | "name": "OBJECT", 2070 | "description": "Location adjacent to an object type definition.", 2071 | "isDeprecated": false, 2072 | "deprecationReason": null 2073 | }, 2074 | { 2075 | "name": "FIELD_DEFINITION", 2076 | "description": "Location adjacent to a field definition.", 2077 | "isDeprecated": false, 2078 | "deprecationReason": null 2079 | }, 2080 | { 2081 | "name": "ARGUMENT_DEFINITION", 2082 | "description": "Location adjacent to an argument definition.", 2083 | "isDeprecated": false, 2084 | "deprecationReason": null 2085 | }, 2086 | { 2087 | "name": "INTERFACE", 2088 | "description": "Location adjacent to an interface definition.", 2089 | "isDeprecated": false, 2090 | "deprecationReason": null 2091 | }, 2092 | { 2093 | "name": "UNION", 2094 | "description": "Location adjacent to a union definition.", 2095 | "isDeprecated": false, 2096 | "deprecationReason": null 2097 | }, 2098 | { 2099 | "name": "ENUM", 2100 | "description": "Location adjacent to an enum definition.", 2101 | "isDeprecated": false, 2102 | "deprecationReason": null 2103 | }, 2104 | { 2105 | "name": "ENUM_VALUE", 2106 | "description": "Location adjacent to an enum value definition.", 2107 | "isDeprecated": false, 2108 | "deprecationReason": null 2109 | }, 2110 | { 2111 | "name": "INPUT_OBJECT", 2112 | "description": "Location adjacent to an input object type definition.", 2113 | "isDeprecated": false, 2114 | "deprecationReason": null 2115 | }, 2116 | { 2117 | "name": "INPUT_FIELD_DEFINITION", 2118 | "description": "Location adjacent to an input object field definition.", 2119 | "isDeprecated": false, 2120 | "deprecationReason": null 2121 | } 2122 | ], 2123 | "possibleTypes": null 2124 | } 2125 | ], 2126 | "directives": [ 2127 | { 2128 | "name": "include", 2129 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 2130 | "locations": [ 2131 | "FIELD", 2132 | "FRAGMENT_SPREAD", 2133 | "INLINE_FRAGMENT" 2134 | ], 2135 | "args": [ 2136 | { 2137 | "name": "if", 2138 | "description": "Included when true.", 2139 | "type": { 2140 | "kind": "NON_NULL", 2141 | "name": null, 2142 | "ofType": { 2143 | "kind": "SCALAR", 2144 | "name": "Boolean", 2145 | "ofType": null 2146 | } 2147 | }, 2148 | "defaultValue": null 2149 | } 2150 | ] 2151 | }, 2152 | { 2153 | "name": "skip", 2154 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 2155 | "locations": [ 2156 | "FIELD", 2157 | "FRAGMENT_SPREAD", 2158 | "INLINE_FRAGMENT" 2159 | ], 2160 | "args": [ 2161 | { 2162 | "name": "if", 2163 | "description": "Skipped when true.", 2164 | "type": { 2165 | "kind": "NON_NULL", 2166 | "name": null, 2167 | "ofType": { 2168 | "kind": "SCALAR", 2169 | "name": "Boolean", 2170 | "ofType": null 2171 | } 2172 | }, 2173 | "defaultValue": null 2174 | } 2175 | ] 2176 | }, 2177 | { 2178 | "name": "deprecated", 2179 | "description": "Marks an element of a GraphQL schema as no longer supported.", 2180 | "locations": [ 2181 | "FIELD_DEFINITION", 2182 | "ENUM_VALUE" 2183 | ], 2184 | "args": [ 2185 | { 2186 | "name": "reason", 2187 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", 2188 | "type": { 2189 | "kind": "SCALAR", 2190 | "name": "String", 2191 | "ofType": null 2192 | }, 2193 | "defaultValue": "\"No longer supported\"" 2194 | } 2195 | ] 2196 | } 2197 | ] 2198 | } 2199 | } 2200 | } -------------------------------------------------------------------------------- /data/services/cartService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/26/16. 3 | */ 4 | import Cart from '../models/Cart'; 5 | import CartEntry from '../models/CartEntry'; 6 | import Product from '../models/Product'; 7 | export const cartService = { 8 | addToCart: (cart, productCode, quantity)=> { 9 | let cartEntry = cart.entries.find((entry)=>entry.product.productCode === productCode); 10 | const product = Product.findOne({ productCode }); 11 | 12 | if (cartEntry) { 13 | cartEntry.quantity = quantity; 14 | cartEntry.price = product.price; 15 | cartEntry.totalPrice = (product.price * quantity).toFixed(2); 16 | } else { 17 | cartEntry = new CartEntry({ id: product.id, product, quantity }); 18 | cart.entries.push(cartEntry); 19 | } 20 | 21 | return cartEntry; 22 | }, 23 | removeFromCart: (cart, cartEntryId)=> { 24 | cart.entries = cart.entries.filter((entry)=>entry.id !== cartEntryId); 25 | }, 26 | createNewCart: (session)=> { 27 | const cart = new Cart({ 28 | entries: [], 29 | }); 30 | 31 | Object.defineProperty(session, 'cart', { 32 | enumerable: true, 33 | writeable: true, 34 | value: cart, 35 | }); 36 | 37 | return cart; 38 | }, 39 | getSessionCart: (session)=> { 40 | let cart = session.cart; 41 | 42 | if (!cart) { 43 | cart = cartService.createNewCart(session); 44 | } 45 | 46 | return cart; 47 | }, 48 | }; 49 | 50 | export default cartService; 51 | -------------------------------------------------------------------------------- /data/services/productService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | 5 | import ProductList from '../models/ProductList'; 6 | import Product from '../models/Product'; 7 | 8 | export const productService = { 9 | findAll: ({ start, size }) => ProductList.findAll({ start, size }), 10 | findOne: ({ productCode }) => Product.findOne({ productCode }), 11 | findById: id => Product.findById(id), 12 | }; 13 | 14 | export default productService; 15 | -------------------------------------------------------------------------------- /data/types/cartEntryType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/26/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | toGlobalId, 27 | } from 'graphql-relay'; 28 | 29 | import logger from '../../logger'; 30 | import productType from './productType'; 31 | 32 | import { nodeInterface } from '../defaultDefinitions'; 33 | export const cartEntryType = new GraphQLObjectType({ 34 | name: 'CartEntry', 35 | fields: () => ({ 36 | id: globalIdField('CartEntry'), 37 | product: { 38 | type: productType, 39 | }, 40 | quantity: { 41 | type: GraphQLFloat, 42 | }, 43 | price: { 44 | type: GraphQLFloat, 45 | }, 46 | totalPrice: { 47 | type: GraphQLFloat, 48 | }, 49 | }), 50 | interfaces: [nodeInterface], 51 | }); 52 | 53 | export const { connectionType: cartEntryConnectionType, edgeType: cartEntryEdgeType } = 54 | connectionDefinitions({ name: 'CartEntry', nodeType: cartEntryType }); 55 | 56 | export const queryCartEntry = { 57 | type: cartEntryType, 58 | args: { 59 | id: { 60 | type: GraphQLID, 61 | }, 62 | }, 63 | resolve: ({}, { id }) => { 64 | logger.info('Resolving queryCartEntry with params:', { id }); 65 | 66 | return {}; 67 | }, 68 | }; 69 | 70 | 71 | export default cartEntryType; 72 | -------------------------------------------------------------------------------- /data/types/cartType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | cursorToOffset, 23 | fromGlobalId, 24 | globalIdField, 25 | mutationWithClientMutationId, 26 | nodeDefinitions, 27 | toGlobalId, 28 | } from 'graphql-relay'; 29 | 30 | import logger from '../../logger'; 31 | import cartEntryType, { cartEntryConnectionType } from './cartEntryType'; 32 | 33 | import cartService from '../services/cartService'; 34 | 35 | import { nodeInterface } from '../defaultDefinitions'; 36 | export const cartType = new GraphQLObjectType({ 37 | name: 'Cart', 38 | fields: () => ({ 39 | id: globalIdField('Cart'), 40 | entries: { 41 | type: cartEntryConnectionType, 42 | args: connectionArgs, 43 | resolve: ({ entries }, args) => { 44 | logger.info('Resolving cartType.entries with params:', args); 45 | 46 | const connection = connectionFromArray( 47 | entries, 48 | args, 49 | ); 50 | return connection; 51 | }, 52 | }, 53 | totalNumberOfItems: { 54 | type: GraphQLInt, 55 | }, 56 | totalPriceOfItems: { 57 | type: GraphQLFloat, 58 | }, 59 | }), 60 | interfaces: [nodeInterface], 61 | }); 62 | 63 | export const { connectionType: cartConnectionType, edgeType: cartEdgeType } = 64 | connectionDefinitions({ 65 | name: 'Cart', 66 | nodeType: cartType, 67 | connectionFields: { 68 | totalNumberOfItems: { 69 | type: GraphQLInt, 70 | }, 71 | }, 72 | }); 73 | 74 | export const queryCart = { 75 | type: cartType, 76 | args: {}, 77 | resolve: ({}, {}, session) => { 78 | logger.info('Resolving queryCart with params:', {}); 79 | 80 | return cartService.getSessionCart(session); 81 | }, 82 | }; 83 | 84 | 85 | export default cartType; 86 | -------------------------------------------------------------------------------- /data/types/imageType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/24/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | toGlobalId, 27 | } from 'graphql-relay'; 28 | 29 | import logger from '../../logger'; 30 | 31 | import { nodeInterface } from '../defaultDefinitions'; 32 | export const imageType = new GraphQLObjectType({ 33 | name: 'Image', 34 | description: 'Just image', 35 | fields: () => ({ 36 | format: { 37 | type: GraphQLString, 38 | description: 'The format of image.', 39 | }, 40 | url: { 41 | type: GraphQLString, 42 | description: 'The url of image.', 43 | }, 44 | }), 45 | }); 46 | 47 | export const { connectionType: imageConnection, edgeType: imageEdge } = 48 | connectionDefinitions({ name: 'Image', nodeType: imageType }); 49 | 50 | export const queryImage = { 51 | type: imageType, 52 | args: { 53 | id: { 54 | type: GraphQLID, 55 | }, 56 | }, 57 | resolve: ({}, { id }) => { 58 | logger.info('Resolving queryImage with params:', { id }); 59 | 60 | return {}; 61 | }, 62 | }; 63 | 64 | 65 | export default imageType; 66 | -------------------------------------------------------------------------------- /data/types/productListType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | cursorToOffset, 23 | fromGlobalId, 24 | globalIdField, 25 | mutationWithClientMutationId, 26 | nodeDefinitions, 27 | toGlobalId, 28 | } from 'graphql-relay'; 29 | 30 | import logger from '../../logger'; 31 | import { productConnectionType } from './productType'; 32 | 33 | import productService from '../services/productService'; 34 | 35 | import { nodeInterface } from '../defaultDefinitions'; 36 | export const productListType = new GraphQLObjectType({ 37 | name: 'ProductList', 38 | fields: () => ({ 39 | id: globalIdField('ProductList'), 40 | items: { 41 | type: productConnectionType, 42 | args: connectionArgs, 43 | resolve: ({}, args) => { 44 | logger.info('Resolving queryProductList with params:', { args }); 45 | const start = args.after ? cursorToOffset(args.after) + 1 : 0; 46 | const size = (args.first || 8) + 1; 47 | 48 | const result = productService.findAll({ start, size }); 49 | 50 | // support pagination 51 | const array = args.after ? new Array(start).concat(result.items) : result.items; 52 | 53 | return connectionFromArray( 54 | array, 55 | args, 56 | ); 57 | }, 58 | }, 59 | }), 60 | interfaces: [nodeInterface], 61 | }); 62 | 63 | export const { connectionType: productListConnectionType, edgeType: productListEdgeType } = 64 | connectionDefinitions({ name: 'ProductList', nodeType: productListType }); 65 | 66 | export const queryProductList = { 67 | type: productListType, 68 | args: {}, 69 | resolve: ({}) => { 70 | logger.info('Resolving queryProductList with params:', {}); 71 | 72 | return {}; 73 | }, 74 | }; 75 | 76 | 77 | export default productListType; 78 | -------------------------------------------------------------------------------- /data/types/productType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | toGlobalId, 27 | } from 'graphql-relay'; 28 | 29 | import logger from '../../logger'; 30 | import imageType from './imageType'; 31 | 32 | import { nodeInterface } from '../defaultDefinitions'; 33 | import productService from '../services/productService'; 34 | 35 | 36 | export const productType = new GraphQLObjectType({ 37 | name: 'Product', 38 | fields: () => ({ 39 | id: globalIdField('Product'), 40 | name: { 41 | type: GraphQLString, 42 | description: '商品名称', 43 | }, 44 | color: { 45 | type: GraphQLString, 46 | description: '商品颜色', 47 | }, 48 | description: { 49 | type: GraphQLString, 50 | description: '商品描述', 51 | }, 52 | price: { 53 | type: GraphQLFloat, 54 | description: '商品价格,保留两位小数', 55 | }, 56 | images: { 57 | type: new GraphQLList(imageType), 58 | description: "商品图片,['primary': 主图,'thumbnail':缩略图,'zoom':大图]", 59 | args: { 60 | format: { 61 | type: GraphQLString, 62 | defaultValue: 'any', 63 | }, 64 | ...connectionArgs, 65 | }, 66 | resolve: async ({ images }, { format, ...args }) => { 67 | if (format !== 'any') { 68 | images = images.filter((image) => image.format === format); 69 | } 70 | return images.slice(0, args.first); 71 | }, 72 | }, 73 | }), 74 | interfaces: [nodeInterface], 75 | }); 76 | 77 | export const { connectionType: productConnectionType, edgeType: productEdgeType } = 78 | connectionDefinitions({ 79 | name: 'Product', 80 | nodeType: productType, 81 | connectionFields: { 82 | totalNumberOfItems: { 83 | type: GraphQLInt, 84 | }, 85 | }, 86 | }); 87 | 88 | export const queryProduct = { 89 | type: productType, 90 | args: { 91 | id: { 92 | type: GraphQLID, 93 | }, 94 | }, 95 | resolve: ({}, { id }) => { 96 | logger.info('Resolving queryProduct with params:', { id }); 97 | const result = productService.findAll({ start: 0, size: 100 }); 98 | 99 | return result.items.find(p => p.id === id); 100 | }, 101 | }; 102 | 103 | 104 | export default productType; 105 | -------------------------------------------------------------------------------- /data/types/productsType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xin on 17/04/2017. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, cursorToOffset, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | toGlobalId, 27 | } from 'graphql-relay'; 28 | 29 | import { nodeInterface } from '../defaultDefinitions'; 30 | 31 | import logger from '../../logger'; 32 | import { productConnectionType } from './productType'; 33 | import productService from '../services/productService'; 34 | 35 | 36 | export const productsType = new GraphQLObjectType({ 37 | name: 'Products', 38 | fields: () => ({ 39 | id: globalIdField('Products'), 40 | items: { 41 | type: productConnectionType, 42 | args: connectionArgs, 43 | resolve: ({ items, totalNumberOfItems }, args) => { 44 | logger.info('Resolving queryProductList with params:', { args }); 45 | // const start = args.after ? cursorToOffset(args.after) + 1 : 0; 46 | // const size = (args.first || 8) + 1; 47 | 48 | // const result = productService.findAll({ start, size }); 49 | 50 | // support pagination 51 | // const array = args.after ? new Array(start).concat(result.items) : result.items; 52 | const connection = connectionFromArray( 53 | items, 54 | args, 55 | ); 56 | 57 | connection.totalElements = totalNumberOfItems; 58 | connection.pageInfo.hasNextPage = !args.last; 59 | connection.pageInfo.hasPreviousPage = !args.first; 60 | 61 | return connection; 62 | }, 63 | }, 64 | }), 65 | interfaces: [nodeInterface], 66 | }); 67 | 68 | export const { connectionType: productsConnectionType, edgeType: productsEdgeType } = 69 | connectionDefinitions({ 70 | name: 'Products', 71 | nodeType: productsType, 72 | connectionFields: { 73 | totalNumberOfItems: { 74 | type: GraphQLInt, 75 | }, 76 | }, 77 | }); 78 | 79 | export const queryProducts = { 80 | type: productsType, 81 | args: { 82 | id: { 83 | type: GraphQLID, 84 | }, 85 | }, 86 | resolve: async (rootValue, { id }, { api }) => { 87 | logger.info('Resolving queryProducts with params:', { id }); 88 | 89 | return {}; 90 | }, 91 | }; 92 | 93 | 94 | export default productsType; 95 | -------------------------------------------------------------------------------- /data/types/viewerType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xin on 17/04/2017. 3 | */ 4 | 5 | import { 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLSchema, 14 | GraphQLString, 15 | } from 'graphql'; 16 | 17 | import { 18 | connectionArgs, 19 | connectionDefinitions, 20 | connectionFromArray, 21 | cursorForObjectInConnection, cursorToOffset, 22 | fromGlobalId, 23 | globalIdField, 24 | mutationWithClientMutationId, 25 | nodeDefinitions, 26 | toGlobalId, 27 | } from 'graphql-relay'; 28 | 29 | import { nodeInterface } from '../defaultDefinitions'; 30 | 31 | import logger from '../../logger'; 32 | import { cartType } from './cartType'; 33 | import cartService from '../services/cartService'; 34 | import productService from '../services/productService'; 35 | import { productsType } from './productsType'; 36 | import { productConnectionType } from './productType'; 37 | 38 | 39 | export const viewerType = new GraphQLObjectType({ 40 | name: 'Viewer', 41 | fields: () => ({ 42 | id: globalIdField('Viewer'), 43 | name: { 44 | type: GraphQLString, 45 | }, 46 | cart: { 47 | type: cartType, 48 | resolve: ({}, { args }, session) => cartService.getSessionCart(session), 49 | }, 50 | products: { 51 | type: productConnectionType, 52 | args: connectionArgs, 53 | resolve: ({}, args) => { 54 | logger.info('Resolving queryProductList with params:', { args }); 55 | const start = args.after ? cursorToOffset(args.after) + 1 : 0; 56 | const size = (args.first || 8) + 1; 57 | 58 | const result = productService.findAll({ start, size }); 59 | 60 | // support pagination 61 | const array = args.after ? new Array(start).concat(result.items) : result.items; 62 | const connection = connectionFromArray( 63 | array, 64 | args, 65 | ); 66 | 67 | connection.totalNumberOfItems = result.totalNumberOfItems; 68 | connection.pageInfo.hasNextPage = !args.last; 69 | connection.pageInfo.hasPreviousPage = !args.first; 70 | 71 | return connection; 72 | }, 73 | }, 74 | }), 75 | interfaces: [nodeInterface], 76 | }); 77 | 78 | export const { connectionType: viewerConnectionType, edgeType: viewerEdgeType } = 79 | connectionDefinitions({ name: 'Viewer', nodeType: viewerType }); 80 | 81 | export const queryViewer = { 82 | type: viewerType, 83 | args: { 84 | id: { 85 | type: GraphQLID, 86 | }, 87 | }, 88 | resolve: async (rootValue, { id }) => { 89 | logger.info('Resolving queryViewer with params:', { id }); 90 | 91 | return {}; 92 | }, 93 | }; 94 | 95 | 96 | export default viewerType; 97 | -------------------------------------------------------------------------------- /graphql.config.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "README_schema" : "Specifies how to load the GraphQL schema that completion, error highlighting, and documentation is based on in the IDE", 4 | "schema": { 5 | 6 | "README_file" : "Remove 'file' to use request url below. A relative or absolute path to the JSON from a schema introspection query, e.g. '{ data: ... }'. Changes to the file are watched.", 7 | "file": "./data/schema.json", 8 | 9 | "README_request" : "To request the schema from a url instead, remove the 'file' JSON property above (and optionally delete the default graphql.schema.json file).", 10 | "request": { 11 | "url" : "http://localhost:8080/graphql", 12 | "method" : "POST", 13 | "README_postIntrospectionQuery" : "Whether to POST an introspectionQuery to the url. If the url always returns the schema JSON, set to false and consider using GET", 14 | "postIntrospectionQuery" : true, 15 | "README_options" : "See the 'Options' section at https://github.com/then/then-request", 16 | "options" : { 17 | "headers": { 18 | "user-agent" : "JS GraphQL" 19 | } 20 | } 21 | } 22 | 23 | }, 24 | 25 | "README_endpoints": "A list of GraphQL endpoints that can be queried from '.graphql' files in the IDE", 26 | "endpoints" : [ 27 | { 28 | "name": "Default (http://localhost:8080/graphql)", 29 | "url": "http://localhost:8080/graphql", 30 | "options" : { 31 | "headers": { 32 | "user-agent" : "JS GraphQL" 33 | } 34 | } 35 | } 36 | ] 37 | 38 | } -------------------------------------------------------------------------------- /graphql.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "README" : "This is a bare-bones schema generated by JS GraphQL. Replace it with your own schema file or load it from a url by editing graphql.config.json", 3 | "data": { 4 | "__schema": { 5 | "queryType": { 6 | "name": "Query" 7 | }, 8 | "mutationType": { 9 | "name": "Mutation" 10 | }, 11 | "types": [ 12 | { 13 | "kind": "OBJECT", 14 | "name": "Query", 15 | "description": null, 16 | "fields": [ 17 | { 18 | "name": "node", 19 | "description": "Fetches an object given its ID", 20 | "args": [ 21 | { 22 | "name": "id", 23 | "description": "The ID of an object", 24 | "type": { 25 | "kind": "NON_NULL", 26 | "name": null, 27 | "ofType": { 28 | "kind": "SCALAR", 29 | "name": "ID", 30 | "ofType": null 31 | } 32 | }, 33 | "defaultValue": null 34 | } 35 | ], 36 | "type": { 37 | "kind": "INTERFACE", 38 | "name": "Node", 39 | "ofType": null 40 | }, 41 | "isDeprecated": false, 42 | "deprecationReason": null 43 | } 44 | ], 45 | "inputFields": null, 46 | "interfaces": [], 47 | "enumValues": null, 48 | "possibleTypes": null 49 | }, 50 | { 51 | "kind": "SCALAR", 52 | "name": "ID", 53 | "description": null, 54 | "fields": null, 55 | "inputFields": null, 56 | "interfaces": null, 57 | "enumValues": null, 58 | "possibleTypes": null 59 | }, 60 | { 61 | "kind": "INTERFACE", 62 | "name": "Node", 63 | "description": "An object with an ID", 64 | "fields": [ 65 | { 66 | "name": "id", 67 | "description": "The id of the object.", 68 | "args": [], 69 | "type": { 70 | "kind": "NON_NULL", 71 | "name": null, 72 | "ofType": { 73 | "kind": "SCALAR", 74 | "name": "ID", 75 | "ofType": null 76 | } 77 | }, 78 | "isDeprecated": false, 79 | "deprecationReason": null 80 | } 81 | ], 82 | "inputFields": null, 83 | "interfaces": null, 84 | "enumValues": null, 85 | "possibleTypes": [ 86 | ] 87 | }, 88 | { 89 | "kind": "SCALAR", 90 | "name": "String", 91 | "description": null, 92 | "fields": null, 93 | "inputFields": null, 94 | "interfaces": null, 95 | "enumValues": null, 96 | "possibleTypes": null 97 | }, 98 | { 99 | "kind": "SCALAR", 100 | "name": "Int", 101 | "description": null, 102 | "fields": null, 103 | "inputFields": null, 104 | "interfaces": null, 105 | "enumValues": null, 106 | "possibleTypes": null 107 | }, 108 | { 109 | "kind": "OBJECT", 110 | "name": "PageInfo", 111 | "description": "Information about pagination in a connection.", 112 | "fields": [ 113 | { 114 | "name": "hasNextPage", 115 | "description": "When paginating forwards, are there more items?", 116 | "args": [], 117 | "type": { 118 | "kind": "NON_NULL", 119 | "name": null, 120 | "ofType": { 121 | "kind": "SCALAR", 122 | "name": "Boolean", 123 | "ofType": null 124 | } 125 | }, 126 | "isDeprecated": false, 127 | "deprecationReason": null 128 | }, 129 | { 130 | "name": "hasPreviousPage", 131 | "description": "When paginating backwards, are there more items?", 132 | "args": [], 133 | "type": { 134 | "kind": "NON_NULL", 135 | "name": null, 136 | "ofType": { 137 | "kind": "SCALAR", 138 | "name": "Boolean", 139 | "ofType": null 140 | } 141 | }, 142 | "isDeprecated": false, 143 | "deprecationReason": null 144 | }, 145 | { 146 | "name": "startCursor", 147 | "description": "When paginating backwards, the cursor to continue.", 148 | "args": [], 149 | "type": { 150 | "kind": "SCALAR", 151 | "name": "String", 152 | "ofType": null 153 | }, 154 | "isDeprecated": false, 155 | "deprecationReason": null 156 | }, 157 | { 158 | "name": "endCursor", 159 | "description": "When paginating forwards, the cursor to continue.", 160 | "args": [], 161 | "type": { 162 | "kind": "SCALAR", 163 | "name": "String", 164 | "ofType": null 165 | }, 166 | "isDeprecated": false, 167 | "deprecationReason": null 168 | } 169 | ], 170 | "inputFields": null, 171 | "interfaces": [], 172 | "enumValues": null, 173 | "possibleTypes": null 174 | }, 175 | { 176 | "kind": "SCALAR", 177 | "name": "Boolean", 178 | "description": null, 179 | "fields": null, 180 | "inputFields": null, 181 | "interfaces": null, 182 | "enumValues": null, 183 | "possibleTypes": null 184 | }, 185 | { 186 | "kind": "OBJECT", 187 | "name": "Mutation", 188 | "description": null, 189 | "fields": [ 190 | { 191 | "name": "id", 192 | "description": "The id of the object.", 193 | "args": [], 194 | "type": { 195 | "kind": "NON_NULL", 196 | "name": null, 197 | "ofType": { 198 | "kind": "SCALAR", 199 | "name": "ID", 200 | "ofType": null 201 | } 202 | }, 203 | "isDeprecated": false, 204 | "deprecationReason": null 205 | } 206 | ], 207 | "inputFields": null, 208 | "interfaces": [], 209 | "enumValues": null, 210 | "possibleTypes": null 211 | }, 212 | { 213 | "kind": "OBJECT", 214 | "name": "__Schema", 215 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query and mutation operations.", 216 | "fields": [ 217 | { 218 | "name": "types", 219 | "description": "A list of all types supported by this server.", 220 | "args": [], 221 | "type": { 222 | "kind": "NON_NULL", 223 | "name": null, 224 | "ofType": { 225 | "kind": "LIST", 226 | "name": null, 227 | "ofType": { 228 | "kind": "NON_NULL", 229 | "name": null, 230 | "ofType": { 231 | "kind": "OBJECT", 232 | "name": "__Type" 233 | } 234 | } 235 | } 236 | }, 237 | "isDeprecated": false, 238 | "deprecationReason": null 239 | }, 240 | { 241 | "name": "queryType", 242 | "description": "The type that query operations will be rooted at.", 243 | "args": [], 244 | "type": { 245 | "kind": "NON_NULL", 246 | "name": null, 247 | "ofType": { 248 | "kind": "OBJECT", 249 | "name": "__Type", 250 | "ofType": null 251 | } 252 | }, 253 | "isDeprecated": false, 254 | "deprecationReason": null 255 | }, 256 | { 257 | "name": "mutationType", 258 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 259 | "args": [], 260 | "type": { 261 | "kind": "OBJECT", 262 | "name": "__Type", 263 | "ofType": null 264 | }, 265 | "isDeprecated": false, 266 | "deprecationReason": null 267 | }, 268 | { 269 | "name": "directives", 270 | "description": "A list of all directives supported by this server.", 271 | "args": [], 272 | "type": { 273 | "kind": "NON_NULL", 274 | "name": null, 275 | "ofType": { 276 | "kind": "LIST", 277 | "name": null, 278 | "ofType": { 279 | "kind": "NON_NULL", 280 | "name": null, 281 | "ofType": { 282 | "kind": "OBJECT", 283 | "name": "__Directive" 284 | } 285 | } 286 | } 287 | }, 288 | "isDeprecated": false, 289 | "deprecationReason": null 290 | } 291 | ], 292 | "inputFields": null, 293 | "interfaces": [], 294 | "enumValues": null, 295 | "possibleTypes": null 296 | }, 297 | { 298 | "kind": "OBJECT", 299 | "name": "__Type", 300 | "description": null, 301 | "fields": [ 302 | { 303 | "name": "kind", 304 | "description": null, 305 | "args": [], 306 | "type": { 307 | "kind": "NON_NULL", 308 | "name": null, 309 | "ofType": { 310 | "kind": "ENUM", 311 | "name": "__TypeKind", 312 | "ofType": null 313 | } 314 | }, 315 | "isDeprecated": false, 316 | "deprecationReason": null 317 | }, 318 | { 319 | "name": "name", 320 | "description": null, 321 | "args": [], 322 | "type": { 323 | "kind": "SCALAR", 324 | "name": "String", 325 | "ofType": null 326 | }, 327 | "isDeprecated": false, 328 | "deprecationReason": null 329 | }, 330 | { 331 | "name": "description", 332 | "description": null, 333 | "args": [], 334 | "type": { 335 | "kind": "SCALAR", 336 | "name": "String", 337 | "ofType": null 338 | }, 339 | "isDeprecated": false, 340 | "deprecationReason": null 341 | }, 342 | { 343 | "name": "fields", 344 | "description": null, 345 | "args": [ 346 | { 347 | "name": "includeDeprecated", 348 | "description": null, 349 | "type": { 350 | "kind": "SCALAR", 351 | "name": "Boolean", 352 | "ofType": null 353 | }, 354 | "defaultValue": "false" 355 | } 356 | ], 357 | "type": { 358 | "kind": "LIST", 359 | "name": null, 360 | "ofType": { 361 | "kind": "NON_NULL", 362 | "name": null, 363 | "ofType": { 364 | "kind": "OBJECT", 365 | "name": "__Field", 366 | "ofType": null 367 | } 368 | } 369 | }, 370 | "isDeprecated": false, 371 | "deprecationReason": null 372 | }, 373 | { 374 | "name": "interfaces", 375 | "description": null, 376 | "args": [], 377 | "type": { 378 | "kind": "LIST", 379 | "name": null, 380 | "ofType": { 381 | "kind": "NON_NULL", 382 | "name": null, 383 | "ofType": { 384 | "kind": "OBJECT", 385 | "name": "__Type", 386 | "ofType": null 387 | } 388 | } 389 | }, 390 | "isDeprecated": false, 391 | "deprecationReason": null 392 | }, 393 | { 394 | "name": "possibleTypes", 395 | "description": null, 396 | "args": [], 397 | "type": { 398 | "kind": "LIST", 399 | "name": null, 400 | "ofType": { 401 | "kind": "NON_NULL", 402 | "name": null, 403 | "ofType": { 404 | "kind": "OBJECT", 405 | "name": "__Type", 406 | "ofType": null 407 | } 408 | } 409 | }, 410 | "isDeprecated": false, 411 | "deprecationReason": null 412 | }, 413 | { 414 | "name": "enumValues", 415 | "description": null, 416 | "args": [ 417 | { 418 | "name": "includeDeprecated", 419 | "description": null, 420 | "type": { 421 | "kind": "SCALAR", 422 | "name": "Boolean", 423 | "ofType": null 424 | }, 425 | "defaultValue": "false" 426 | } 427 | ], 428 | "type": { 429 | "kind": "LIST", 430 | "name": null, 431 | "ofType": { 432 | "kind": "NON_NULL", 433 | "name": null, 434 | "ofType": { 435 | "kind": "OBJECT", 436 | "name": "__EnumValue", 437 | "ofType": null 438 | } 439 | } 440 | }, 441 | "isDeprecated": false, 442 | "deprecationReason": null 443 | }, 444 | { 445 | "name": "inputFields", 446 | "description": null, 447 | "args": [], 448 | "type": { 449 | "kind": "LIST", 450 | "name": null, 451 | "ofType": { 452 | "kind": "NON_NULL", 453 | "name": null, 454 | "ofType": { 455 | "kind": "OBJECT", 456 | "name": "__InputValue", 457 | "ofType": null 458 | } 459 | } 460 | }, 461 | "isDeprecated": false, 462 | "deprecationReason": null 463 | }, 464 | { 465 | "name": "ofType", 466 | "description": null, 467 | "args": [], 468 | "type": { 469 | "kind": "OBJECT", 470 | "name": "__Type", 471 | "ofType": null 472 | }, 473 | "isDeprecated": false, 474 | "deprecationReason": null 475 | } 476 | ], 477 | "inputFields": null, 478 | "interfaces": [], 479 | "enumValues": null, 480 | "possibleTypes": null 481 | }, 482 | { 483 | "kind": "ENUM", 484 | "name": "__TypeKind", 485 | "description": "An enum describing what kind of type a given __Type is", 486 | "fields": null, 487 | "inputFields": null, 488 | "interfaces": null, 489 | "enumValues": [ 490 | { 491 | "name": "SCALAR", 492 | "description": "Indicates this type is a scalar.", 493 | "isDeprecated": false, 494 | "deprecationReason": null 495 | }, 496 | { 497 | "name": "OBJECT", 498 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 499 | "isDeprecated": false, 500 | "deprecationReason": null 501 | }, 502 | { 503 | "name": "INTERFACE", 504 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 505 | "isDeprecated": false, 506 | "deprecationReason": null 507 | }, 508 | { 509 | "name": "UNION", 510 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 511 | "isDeprecated": false, 512 | "deprecationReason": null 513 | }, 514 | { 515 | "name": "ENUM", 516 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 517 | "isDeprecated": false, 518 | "deprecationReason": null 519 | }, 520 | { 521 | "name": "INPUT_OBJECT", 522 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 523 | "isDeprecated": false, 524 | "deprecationReason": null 525 | }, 526 | { 527 | "name": "LIST", 528 | "description": "Indicates this type is a list. `ofType` is a valid field.", 529 | "isDeprecated": false, 530 | "deprecationReason": null 531 | }, 532 | { 533 | "name": "NON_NULL", 534 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 535 | "isDeprecated": false, 536 | "deprecationReason": null 537 | } 538 | ], 539 | "possibleTypes": null 540 | }, 541 | { 542 | "kind": "OBJECT", 543 | "name": "__Field", 544 | "description": null, 545 | "fields": [ 546 | { 547 | "name": "name", 548 | "description": null, 549 | "args": [], 550 | "type": { 551 | "kind": "NON_NULL", 552 | "name": null, 553 | "ofType": { 554 | "kind": "SCALAR", 555 | "name": "String", 556 | "ofType": null 557 | } 558 | }, 559 | "isDeprecated": false, 560 | "deprecationReason": null 561 | }, 562 | { 563 | "name": "description", 564 | "description": null, 565 | "args": [], 566 | "type": { 567 | "kind": "SCALAR", 568 | "name": "String", 569 | "ofType": null 570 | }, 571 | "isDeprecated": false, 572 | "deprecationReason": null 573 | }, 574 | { 575 | "name": "args", 576 | "description": null, 577 | "args": [], 578 | "type": { 579 | "kind": "NON_NULL", 580 | "name": null, 581 | "ofType": { 582 | "kind": "LIST", 583 | "name": null, 584 | "ofType": { 585 | "kind": "NON_NULL", 586 | "name": null, 587 | "ofType": { 588 | "kind": "OBJECT", 589 | "name": "__InputValue" 590 | } 591 | } 592 | } 593 | }, 594 | "isDeprecated": false, 595 | "deprecationReason": null 596 | }, 597 | { 598 | "name": "type", 599 | "description": null, 600 | "args": [], 601 | "type": { 602 | "kind": "NON_NULL", 603 | "name": null, 604 | "ofType": { 605 | "kind": "OBJECT", 606 | "name": "__Type", 607 | "ofType": null 608 | } 609 | }, 610 | "isDeprecated": false, 611 | "deprecationReason": null 612 | }, 613 | { 614 | "name": "isDeprecated", 615 | "description": null, 616 | "args": [], 617 | "type": { 618 | "kind": "NON_NULL", 619 | "name": null, 620 | "ofType": { 621 | "kind": "SCALAR", 622 | "name": "Boolean", 623 | "ofType": null 624 | } 625 | }, 626 | "isDeprecated": false, 627 | "deprecationReason": null 628 | }, 629 | { 630 | "name": "deprecationReason", 631 | "description": null, 632 | "args": [], 633 | "type": { 634 | "kind": "SCALAR", 635 | "name": "String", 636 | "ofType": null 637 | }, 638 | "isDeprecated": false, 639 | "deprecationReason": null 640 | } 641 | ], 642 | "inputFields": null, 643 | "interfaces": [], 644 | "enumValues": null, 645 | "possibleTypes": null 646 | }, 647 | { 648 | "kind": "OBJECT", 649 | "name": "__InputValue", 650 | "description": null, 651 | "fields": [ 652 | { 653 | "name": "name", 654 | "description": null, 655 | "args": [], 656 | "type": { 657 | "kind": "NON_NULL", 658 | "name": null, 659 | "ofType": { 660 | "kind": "SCALAR", 661 | "name": "String", 662 | "ofType": null 663 | } 664 | }, 665 | "isDeprecated": false, 666 | "deprecationReason": null 667 | }, 668 | { 669 | "name": "description", 670 | "description": null, 671 | "args": [], 672 | "type": { 673 | "kind": "SCALAR", 674 | "name": "String", 675 | "ofType": null 676 | }, 677 | "isDeprecated": false, 678 | "deprecationReason": null 679 | }, 680 | { 681 | "name": "type", 682 | "description": null, 683 | "args": [], 684 | "type": { 685 | "kind": "NON_NULL", 686 | "name": null, 687 | "ofType": { 688 | "kind": "OBJECT", 689 | "name": "__Type", 690 | "ofType": null 691 | } 692 | }, 693 | "isDeprecated": false, 694 | "deprecationReason": null 695 | }, 696 | { 697 | "name": "defaultValue", 698 | "description": null, 699 | "args": [], 700 | "type": { 701 | "kind": "SCALAR", 702 | "name": "String", 703 | "ofType": null 704 | }, 705 | "isDeprecated": false, 706 | "deprecationReason": null 707 | } 708 | ], 709 | "inputFields": null, 710 | "interfaces": [], 711 | "enumValues": null, 712 | "possibleTypes": null 713 | }, 714 | { 715 | "kind": "OBJECT", 716 | "name": "__EnumValue", 717 | "description": null, 718 | "fields": [ 719 | { 720 | "name": "name", 721 | "description": null, 722 | "args": [], 723 | "type": { 724 | "kind": "NON_NULL", 725 | "name": null, 726 | "ofType": { 727 | "kind": "SCALAR", 728 | "name": "String", 729 | "ofType": null 730 | } 731 | }, 732 | "isDeprecated": false, 733 | "deprecationReason": null 734 | }, 735 | { 736 | "name": "description", 737 | "description": null, 738 | "args": [], 739 | "type": { 740 | "kind": "SCALAR", 741 | "name": "String", 742 | "ofType": null 743 | }, 744 | "isDeprecated": false, 745 | "deprecationReason": null 746 | }, 747 | { 748 | "name": "isDeprecated", 749 | "description": null, 750 | "args": [], 751 | "type": { 752 | "kind": "NON_NULL", 753 | "name": null, 754 | "ofType": { 755 | "kind": "SCALAR", 756 | "name": "Boolean", 757 | "ofType": null 758 | } 759 | }, 760 | "isDeprecated": false, 761 | "deprecationReason": null 762 | }, 763 | { 764 | "name": "deprecationReason", 765 | "description": null, 766 | "args": [], 767 | "type": { 768 | "kind": "SCALAR", 769 | "name": "String", 770 | "ofType": null 771 | }, 772 | "isDeprecated": false, 773 | "deprecationReason": null 774 | } 775 | ], 776 | "inputFields": null, 777 | "interfaces": [], 778 | "enumValues": null, 779 | "possibleTypes": null 780 | }, 781 | { 782 | "kind": "OBJECT", 783 | "name": "__Directive", 784 | "description": null, 785 | "fields": [ 786 | { 787 | "name": "name", 788 | "description": null, 789 | "args": [], 790 | "type": { 791 | "kind": "NON_NULL", 792 | "name": null, 793 | "ofType": { 794 | "kind": "SCALAR", 795 | "name": "String", 796 | "ofType": null 797 | } 798 | }, 799 | "isDeprecated": false, 800 | "deprecationReason": null 801 | }, 802 | { 803 | "name": "description", 804 | "description": null, 805 | "args": [], 806 | "type": { 807 | "kind": "SCALAR", 808 | "name": "String", 809 | "ofType": null 810 | }, 811 | "isDeprecated": false, 812 | "deprecationReason": null 813 | }, 814 | { 815 | "name": "args", 816 | "description": null, 817 | "args": [], 818 | "type": { 819 | "kind": "NON_NULL", 820 | "name": null, 821 | "ofType": { 822 | "kind": "LIST", 823 | "name": null, 824 | "ofType": { 825 | "kind": "NON_NULL", 826 | "name": null, 827 | "ofType": { 828 | "kind": "OBJECT", 829 | "name": "__InputValue" 830 | } 831 | } 832 | } 833 | }, 834 | "isDeprecated": false, 835 | "deprecationReason": null 836 | }, 837 | { 838 | "name": "onOperation", 839 | "description": null, 840 | "args": [], 841 | "type": { 842 | "kind": "NON_NULL", 843 | "name": null, 844 | "ofType": { 845 | "kind": "SCALAR", 846 | "name": "Boolean", 847 | "ofType": null 848 | } 849 | }, 850 | "isDeprecated": false, 851 | "deprecationReason": null 852 | }, 853 | { 854 | "name": "onFragment", 855 | "description": null, 856 | "args": [], 857 | "type": { 858 | "kind": "NON_NULL", 859 | "name": null, 860 | "ofType": { 861 | "kind": "SCALAR", 862 | "name": "Boolean", 863 | "ofType": null 864 | } 865 | }, 866 | "isDeprecated": false, 867 | "deprecationReason": null 868 | }, 869 | { 870 | "name": "onField", 871 | "description": null, 872 | "args": [], 873 | "type": { 874 | "kind": "NON_NULL", 875 | "name": null, 876 | "ofType": { 877 | "kind": "SCALAR", 878 | "name": "Boolean", 879 | "ofType": null 880 | } 881 | }, 882 | "isDeprecated": false, 883 | "deprecationReason": null 884 | } 885 | ], 886 | "inputFields": null, 887 | "interfaces": [], 888 | "enumValues": null, 889 | "possibleTypes": null 890 | } 891 | ], 892 | "directives": [ 893 | { 894 | "name": "include", 895 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 896 | "args": [ 897 | { 898 | "name": "if", 899 | "description": "Included when true.", 900 | "type": { 901 | "kind": "NON_NULL", 902 | "name": null, 903 | "ofType": { 904 | "kind": "SCALAR", 905 | "name": "Boolean", 906 | "ofType": null 907 | } 908 | }, 909 | "defaultValue": null 910 | } 911 | ], 912 | "onOperation": false, 913 | "onFragment": true, 914 | "onField": true 915 | }, 916 | { 917 | "name": "skip", 918 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 919 | "args": [ 920 | { 921 | "name": "if", 922 | "description": "Skipped when true.", 923 | "type": { 924 | "kind": "NON_NULL", 925 | "name": null, 926 | "ofType": { 927 | "kind": "SCALAR", 928 | "name": "Boolean", 929 | "ofType": null 930 | } 931 | }, 932 | "defaultValue": null 933 | } 934 | ], 935 | "onOperation": false, 936 | "onFragment": true, 937 | "onField": true 938 | } 939 | ] 940 | } 941 | } 942 | } -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 10/18/2015. 3 | */ 4 | import requireDir from 'require-dir'; 5 | let tasks = requireDir('./tasks'); 6 | 7 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { Router, browserHistory, applyRouterMiddleware } from 'react-router'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Relay from 'react-relay'; 6 | import useRelay from 'react-router-relay'; 7 | import FastClick from 'fastclick'; 8 | 9 | import rootRoute from './routes/rootRoute'; 10 | 11 | const networkLayerOptions = { 12 | fetchTimeout: 30000, // Timeout after 30s. 13 | retryDelays: [5000], // Only retry once after a 5s delay. 14 | credentials: 'same-origin', // pass cookies when request. 15 | }; 16 | 17 | /* inject DefaultNetworkLayer with options */ 18 | Relay.injectNetworkLayer( 19 | new Relay.DefaultNetworkLayer('/graphql', networkLayerOptions), 20 | ); 21 | 22 | ReactDOM.render( 23 | , 29 | document.getElementById('root'), 30 | ); 31 | 32 | document.addEventListener('DOMContentLoaded', () => { 33 | FastClick.attach(document.body); 34 | }, false); 35 | -------------------------------------------------------------------------------- /js/components/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | 7 | 8 | /* import base styles */ 9 | import 'normalize.css/normalize.css'; 10 | import './styles/animations.scss'; 11 | import './styles/icons.scss'; 12 | import 'ionicons/dist/scss/ionicons.scss'; 13 | import './styles/shape.scss'; 14 | 15 | /* import common styles */ 16 | import './styles/common.scss'; 17 | import './styles/entries.scss'; 18 | 19 | class App extends React.Component { 20 | 21 | static propTypes = { 22 | children: PropTypes.element, 23 | }; 24 | 25 | static contextTypes = { 26 | router: PropTypes.object.isRequired, 27 | }; 28 | 29 | render() { 30 | const { router } = this.context; 31 | const { children } = this.props; 32 | 33 | if (!children) { 34 | router.push('/product-list'); 35 | return null; 36 | } 37 | return children; 38 | } 39 | } 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /js/components/cart/Cart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/26/16. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Relay from 'react-relay'; 8 | import { Map } from 'immutable'; 9 | import debounce from 'lodash/debounce'; 10 | import classNames from 'classnames'; 11 | 12 | import Price from '../common/Price'; 13 | 14 | import AddToCartMutation from '../../mutations/AddToCartMutation'; 15 | import RemoveFromCartMutation from '../../mutations/RemoveFromCartMutation'; 16 | 17 | import CartEntry from '../cart/CartEntry'; 18 | import InputQuantity from '../form/InputQuantity'; 19 | 20 | import './Cart.scss'; 21 | 22 | class Cart extends React.Component { 23 | 24 | static propTypes = { 25 | cart: PropTypes.object, 26 | }; 27 | 28 | static contextTypes = { 29 | router: PropTypes.object.isRequired, 30 | }; 31 | 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | data: Map({}), 36 | }; 37 | } 38 | 39 | setImmState = (fn)=> 40 | this.setState(({ data }) => ({ 41 | data: fn(data), 42 | })); 43 | 44 | addToCartDebounce = debounce(()=> { 45 | this.addToCartTransaction.commit(); 46 | this.addToCartTransaction = null; 47 | }, 500); 48 | 49 | addToCart = (product, quantity)=> { 50 | const { relay, viewer } = this.props; 51 | const { cart } = viewer; 52 | return relay.applyUpdate(new AddToCartMutation({ cart, product, quantity }), { 53 | onSuccess: () => { 54 | console.log('added to cart!'); 55 | }, 56 | onFailure: async(tansition) => { 57 | let errors; 58 | if (tansition.getError().source) { 59 | errors = tansition.getError() && tansition.getError().source.errors; 60 | } else { 61 | errors = (await tansition.getError().json()).errors; 62 | } 63 | errors.forEach((error)=> { 64 | Toast.fail(error.message, 10); 65 | }); 66 | }, 67 | }); 68 | }; 69 | 70 | 71 | removeFromCart = (cartEntry)=> { 72 | const { relay, viewer } = this.props; 73 | const { cart } = viewer; 74 | return relay.applyUpdate(new RemoveFromCartMutation({ cart, cartEntry })); 75 | }; 76 | 77 | /** 78 | * 79 | * @param cartEntry 80 | * @param quantity 81 | */ 82 | handleEntryQuantityChange = async(cartEntry, quantity)=> { 83 | if (this.addToCartTransaction) { 84 | await this.addToCartTransaction.rollback(); 85 | } 86 | if (quantity < 1) { 87 | this.addToCartTransaction = await this.removeFromCart(cartEntry); 88 | } else { 89 | this.addToCartTransaction = await this.addToCart(cartEntry.product, quantity); 90 | } 91 | this.addToCartDebounce(); 92 | }; 93 | 94 | render() { 95 | const { cart } = this.props.viewer; 96 | const {} = this.state.data.toJS(); 97 | 98 | const entries = cart.entries.edges.map(({ node: cartEntry })=> 99 | 100 |
101 | 103 |
104 |
105 | ); 106 | 107 | return ( 108 |
109 |
110 |
111 |

Subtotal ({cart.totalNumberOfItems} items):

112 |
113 | {entries} 114 |
115 |
116 | ); 117 | } 118 | } 119 | 120 | export default Relay.createContainer(Cart, { 121 | 122 | initialVariables: {}, 123 | 124 | fragments: { 125 | viewer: () => Relay.QL` 126 | fragment on Viewer { 127 | cart{ 128 | id 129 | entries(first: 100){ 130 | edges { 131 | node { 132 | id 133 | product{ 134 | id 135 | ${AddToCartMutation.getFragment('product')} 136 | } 137 | quantity 138 | ${RemoveFromCartMutation.getFragment('cartEntry')} 139 | ${CartEntry.getFragment('cartEntry')} 140 | } 141 | } 142 | pageInfo { 143 | hasNextPage 144 | hasPreviousPage 145 | } 146 | } 147 | totalNumberOfItems 148 | totalPriceOfItems 149 | ${AddToCartMutation.getFragment('cart')} 150 | ${RemoveFromCartMutation.getFragment('cart')} 151 | } 152 | } 153 | `, 154 | }, 155 | }); 156 | -------------------------------------------------------------------------------- /js/components/cart/Cart.scss: -------------------------------------------------------------------------------- 1 | .Cart { 2 | margin-top: .32rem; 3 | padding: 0 .32rem; 4 | border-top: 1px solid #cfcfd2; 5 | background: #fff; 6 | } -------------------------------------------------------------------------------- /js/components/cart/CartEntry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/28/16. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Relay from 'react-relay'; 8 | import { Map } from 'immutable'; 9 | import classNames from 'classnames'; 10 | 11 | import Price from '../common/Price'; 12 | 13 | import './CartEntry.scss'; 14 | 15 | 16 | class CartEntry extends React.Component { 17 | 18 | static propTypes = { 19 | product: PropTypes.object, 20 | children: PropTypes.object, 21 | onAddToCartClick: PropTypes.func, 22 | }; 23 | 24 | static contextTypes = { 25 | router: PropTypes.object.isRequired, 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | data: Map({}), 32 | }; 33 | } 34 | 35 | setImmState = (fn)=> 36 | this.setState(({ data }) => ({ 37 | data: fn(data), 38 | })); 39 | 40 | render() { 41 | const { cartEntry, children } = this.props; 42 | const {} = this.state.data.toJS(); 43 | const product = cartEntry.product; 44 | const totalPrice = product.price * cartEntry.quantity; 45 | 46 | return ( 47 |
48 | 49 | 50 | 51 |
52 |
{product.name}
53 | { children } 54 | 55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | 62 | export default Relay.createContainer(CartEntry, { 63 | 64 | fragments: { 65 | cartEntry: () => Relay.QL` 66 | fragment on CartEntry { 67 | id 68 | product{ 69 | id 70 | name 71 | price 72 | images(format: "thumbnail"){ 73 | url 74 | } 75 | } 76 | quantity 77 | } 78 | `, 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /js/components/cart/CartEntry.scss: -------------------------------------------------------------------------------- 1 | .CartEntry { 2 | 3 | .operation { 4 | .icon { 5 | font-size: .6rem; 6 | color: #e76e41; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /js/components/cart/CartWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/25/16. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import ReactDOM from 'react-dom'; 8 | import { Map } from 'immutable'; 9 | import classNames from 'classnames'; 10 | 11 | import Portal from '../common/Portal'; 12 | import CurveBall from '../common/CurveBall'; 13 | 14 | import './CartWidget.scss'; 15 | 16 | export default class CartWidget extends React.Component { 17 | 18 | static propTypes = { 19 | number: PropTypes.number, 20 | onClick: PropTypes.func, 21 | }; 22 | 23 | static contextTypes = { 24 | router: PropTypes.object.isRequired, 25 | }; 26 | 27 | static defaultProps = { 28 | number: 0, 29 | }; 30 | 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | data: Map({ 35 | curveBallWidth: document.documentElement.clientWidth, 36 | curveBallHeight: document.documentElement.clientHeight, 37 | curveBallRadius: window.rem * 0.2, 38 | }), 39 | }; 40 | } 41 | 42 | componentDidMount() { 43 | 44 | window.addEventListener('resize', this.handleResize); 45 | this.cartIconDOM = ReactDOM.findDOMNode(this.cartIcon); 46 | this.cartIconRect = this.cartIconDOM.getBoundingClientRect(); 47 | 48 | this.cartIconDOM.addEventListener('webkitAnimationEnd', function () { 49 | this.classList.remove('scale'); 50 | }); 51 | } 52 | 53 | componentWillUnmount() { 54 | this.cartIconDOM.removeEventListener('webkitAnimationEnd', function () { 55 | 56 | }); 57 | } 58 | 59 | setImmState = (fn)=> 60 | this.setState(({ data }) => ({ 61 | data: fn(data), 62 | })); 63 | 64 | /** 65 | * duration: duration of animation, the smaller the faster 66 | */ 67 | animateCurveBall = (startX, startY, duration = 1000)=> { 68 | const endRect = this.cartIconRect; 69 | const endX = endRect.left + (endRect.width / 2); 70 | const endY = endRect.top; 71 | 72 | const controlX = (startX + endX) / 2; 73 | 74 | // make the movement track of ball more curly 75 | const controlY = Math.min(startY - 150, endY - 150); 76 | 77 | // let's move the ball 78 | this.curveBall.animateCurveBallMoving(startX, startY, controlX, controlY, endX, endY, duration); 79 | }; 80 | 81 | resizeCurveBall = ()=> { 82 | this.cartIconRect = this.cartIconDOM.getBoundingClientRect(); 83 | this.setImmState(d =>d 84 | .set('curveBallWidth', document.documentElement.clientWidth) 85 | .set('curveBallHeight', document.documentElement.clientHeight) 86 | .set('curveBallRadius', 15) 87 | ); 88 | }; 89 | 90 | scale = ()=> { 91 | const { cartIcon } = this.refs; 92 | // 'webkitAnimationEnd' event will finish ahead of time if we don't wrap with setTimeout 93 | // and then 'scale' will not remove from classList 94 | setTimeout(()=> { 95 | cartIcon.classList.add('scale'); 96 | }, 0); 97 | }; 98 | 99 | 100 | handleResize = ()=> { 101 | this.resizeCurveBall(); 102 | }; 103 | 104 | 105 | handleCartWidgetClick = ()=> { 106 | const { onClick } = this.props; 107 | if (typeof onClick === 'function') { 108 | onClick(); 109 | } 110 | }; 111 | 112 | handleCurveBallStepEnd = ()=> { 113 | setTimeout(()=> { 114 | this.cartIconDOM.classList.add('scale'); 115 | }, 0); 116 | }; 117 | 118 | render() { 119 | const { number } = this.props; 120 | const { curveBallWidth, curveBallHeight, curveBallRadius } = this.state.data.toJS(); 121 | const isEmpty = number === 0; 122 | 123 | const iconClassnames = classNames('icon', { 124 | 'ion-ios-cart-outline': isEmpty, 125 | 'ion-ios-cart': !isEmpty, 126 | }); 127 | 128 | return ( 129 | 130 |
131 | { this.cartIcon = c; }} /> 132 | {number} 133 | 134 | { /* simulate a ball moving into the cart when click on plus button */ } 135 | { this.curveBall = c; }} onStepEnd={this.handleCurveBallStepEnd} /> 138 | 139 |
140 | 141 | 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /js/components/cart/CartWidget.scss: -------------------------------------------------------------------------------- 1 | .CartWidget{ 2 | position: absolute; 3 | right: .32rem; 4 | bottom: 2.48rem; 5 | 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | 10 | height: 1.24rem; 11 | width: 1.24rem; 12 | text-align: center; 13 | border: 1px solid #cfcfd2; 14 | border-radius: .62rem; 15 | background: #fff; 16 | 17 | .icon{ 18 | font-size: .8rem; 19 | color:#e76e41; 20 | } 21 | 22 | .bubble{ 23 | position: absolute; 24 | top: -.12rem; 25 | right: -.12rem; 26 | background: #e76e41; 27 | padding: .12rem; 28 | border-radius: 1.24rem; 29 | color: #fff; 30 | } 31 | } -------------------------------------------------------------------------------- /js/components/common/Ball.js: -------------------------------------------------------------------------------- 1 | export default class Ball { 2 | constructor(ctx, radius, color) { 3 | this.ctx = ctx; 4 | this.radius = radius; 5 | this.startAngle = 0; 6 | this.endAngle = 2 * Math.PI; 7 | this.color = color; 8 | } 9 | 10 | draw(x, y) { 11 | this.ctx.beginPath(); 12 | this.ctx.arc(x, y, this.radius, this.startAngle, this.endAngle); 13 | this.ctx.fillStyle = this.color; 14 | this.ctx.fill(); 15 | this.ctx.closePath(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /js/components/common/CurveBall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 11/18/2015. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import ReactDOM from 'react-dom'; 8 | import { Map } from 'immutable'; 9 | import Ball from './Ball'; 10 | 11 | import './CurveBall.scss'; 12 | 13 | export default class CurveBall extends React.Component { 14 | 15 | static propTypes = { 16 | colorOfBall: PropTypes.string, 17 | width: PropTypes.number, 18 | height: PropTypes.number, 19 | radius: PropTypes.number, 20 | 21 | onStepEnd: PropTypes.func, 22 | }; 23 | 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | data: Map({}), 28 | }; 29 | this.start = null; 30 | this.animationQueue = []; 31 | } 32 | 33 | componentDidMount() { 34 | const { canvas } = this.refs; 35 | const { radius, colorOfBall } = this.props; 36 | const canvasDOM = ReactDOM.findDOMNode(canvas); 37 | this.ctx = canvasDOM.getContext('2d'); 38 | this.ball = new Ball(this.ctx, radius, colorOfBall); 39 | } 40 | 41 | setImmState = (fn)=> 42 | this.setState(({ data }) => ({ 43 | data: fn(data), 44 | })); 45 | 46 | cleanRect = ()=> { 47 | this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); 48 | }; 49 | 50 | isAllAnimationEnd = ()=> 51 | this.animationQueue.every((animation)=> !animation || animation.progress >= 1); 52 | 53 | drawBezierSplit = (ball, x0, y0, x1, y1, x2, y2, t0, t1)=> { 54 | // reference http://www.pjgalbraith.com/drawing-animated-curves-javascript/ 55 | if (t0 === 0.0 && t1 === 1.0) { 56 | ball.draw(x2, y2); 57 | } else if (t0 !== t1) { 58 | let t00 = t0 * t0; 59 | let t01 = 1.0 - t0; 60 | let t02 = t01 * t01; 61 | let t03 = 2.0 * t0 * t01; 62 | 63 | t00 = t1 * t1; 64 | t01 = 1.0 - t1; 65 | t02 = t01 * t01; 66 | t03 = 2.0 * t1 * t01; 67 | 68 | const nx2 = t02 * x0 + t03 * x1 + t00 * x2; 69 | const ny2 = t02 * y0 + t03 * y1 + t00 * y2; 70 | 71 | ball.draw(nx2, ny2); 72 | } 73 | }; 74 | 75 | animatePathDrawing = ()=> { 76 | const { onStepEnd } = this.props; 77 | const self = this; 78 | 79 | const step = function animatePathDrawingStep(timestamp) { 80 | // Clear canvas 81 | self.cleanRect(); 82 | 83 | for (let i = 0; i < self.animationQueue.length; ++i) { 84 | if (self.animationQueue[i]) { 85 | const animation = self.animationQueue[i]; 86 | const delta = timestamp - animation.start; 87 | const progress = Math.min(delta / animation.duration, 1); 88 | 89 | // Draw curve 90 | self.drawBezierSplit(self.ball, 91 | animation.startX, animation.startY, 92 | animation.controlX, animation.controlY, 93 | animation.endX, animation.endY, 94 | 0, progress); 95 | 96 | if (progress === 1) { 97 | // release animation when step end 98 | self.animationQueue[i] = undefined; 99 | if (typeof onStepEnd === 'function') onStepEnd(); 100 | } else { 101 | animation.progress = progress; 102 | } 103 | } 104 | } 105 | 106 | if (self.isAllAnimationEnd()) { 107 | self.cleanRect(); 108 | } else { 109 | window.requestAnimationFrame(step); 110 | } 111 | }; 112 | 113 | window.requestAnimationFrame(step); 114 | }; 115 | 116 | animateCurveBallMoving = (startX, startY, controlX, controlY, endX, endY, duration)=> { 117 | const isAllAnimationEnd = this.isAllAnimationEnd(); 118 | 119 | this.animationQueue.push({ 120 | progress: 0, 121 | start: window.performance.now(), 122 | startX, 123 | startY, 124 | controlX, 125 | controlY, 126 | endX, 127 | endY, 128 | duration, 129 | }); 130 | 131 | if (isAllAnimationEnd) { 132 | this.animatePathDrawing(); 133 | } 134 | }; 135 | 136 | render() { 137 | const { width, height } = this.props; 138 | 139 | return ( 140 | 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /js/components/common/CurveBall.scss: -------------------------------------------------------------------------------- 1 | .CurveBall{ 2 | position: absolute; 3 | bottom: 0; 4 | pointer-events: none; 5 | } -------------------------------------------------------------------------------- /js/components/common/Portal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 9/14/2015. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import ReactDOM from 'react-dom'; 8 | import classNames from 'classnames'; 9 | 10 | import './Portal.scss'; 11 | 12 | 13 | export default class Portal extends React.Component { 14 | 15 | static propTypes = { 16 | id: PropTypes.string, 17 | className: PropTypes.string, 18 | hidden: PropTypes.bool, 19 | children: PropTypes.any, 20 | onClick: PropTypes.func, 21 | }; 22 | 23 | static defaultProps = { 24 | id: 'Portal', 25 | }; 26 | 27 | static contextTypes = { 28 | router: PropTypes.object, 29 | }; 30 | 31 | static defaultProps = { 32 | children: null, 33 | }; 34 | 35 | componentDidMount() { 36 | const { id, hidden, className, onClick } = this.props; 37 | let p = id && document.getElementById(id); 38 | if (!p) { 39 | p = document.createElement('div'); 40 | p.id = id; 41 | p.hidden = hidden; 42 | if (className) { 43 | p.className = className; 44 | } 45 | 46 | if (typeof onClick === 'function') { 47 | p.onclick = onClick; 48 | } 49 | 50 | document.body.appendChild(p); 51 | } 52 | this.portalElement = p; 53 | this.componentDidUpdate(); 54 | } 55 | 56 | componentWillReceiveProps(nextProps) { 57 | const { hidden } = nextProps; 58 | // console.log(this); 59 | // const p = this.props.id && document.getElementById(this.props.id); 60 | this.portalElement.hidden = hidden; 61 | } 62 | 63 | componentDidUpdate() { 64 | ReactDOM.render(this.props.children, this.portalElement); 65 | } 66 | 67 | componentWillUnmount() { 68 | if (document.getElementById(this.props.id)) { 69 | ReactDOM.unmountComponentAtNode(this.portalElement); 70 | document.body.removeChild(this.portalElement); 71 | } 72 | } 73 | 74 | render() { 75 | return null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /js/components/common/Portal.scss: -------------------------------------------------------------------------------- 1 | 2 | #Portal { 3 | position: absolute; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | display: flex; 9 | } -------------------------------------------------------------------------------- /js/components/common/Price.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xin on 5/24/16. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import classNames from 'classnames/bind'; 8 | import './Price.scss'; 9 | 10 | export default class Price extends React.Component { 11 | 12 | render() { 13 | const { className, value } = this.props; 14 | 15 | return ( 16 | 17 | {`¥${parseFloat(value).toFixed(2)}`} 18 | 19 | ); 20 | } 21 | } 22 | 23 | Price.propTypes = { 24 | value: PropTypes.oneOfType([ 25 | PropTypes.string, 26 | PropTypes.number, 27 | ]), 28 | className: PropTypes.string, 29 | }; 30 | -------------------------------------------------------------------------------- /js/components/common/Price.scss: -------------------------------------------------------------------------------- 1 | .Price { 2 | color: #e76e41; 3 | } 4 | -------------------------------------------------------------------------------- /js/components/common/Scroll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 9/16/2015. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { Map } from 'immutable'; 8 | import IScroll from 'iscroll/build/iscroll-lite'; 9 | 10 | import './Scroll.scss'; 11 | export default class Scroll extends React.Component { 12 | 13 | static propTypes = { 14 | children: PropTypes.oneOfType([ 15 | PropTypes.element, 16 | PropTypes.arrayOf(PropTypes.element), 17 | ]), 18 | onScrollEnd: PropTypes.func, 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | data: Map({ 25 | x: 0, 26 | y: 0, 27 | }), 28 | }; 29 | } 30 | 31 | componentDidMount = ()=> { 32 | this._init(); 33 | }; 34 | 35 | componentDidUpdate = (/* prevProps, prevState */)=> { 36 | this.scroll.refresh(); 37 | }; 38 | 39 | setImmState = (fn)=> 40 | this.setState(({ data }) => ({ 41 | data: fn(data), 42 | })); 43 | 44 | _init = ()=> { 45 | const self = this; 46 | const elem = this.refs.Scroll; 47 | 48 | const scrollOption = { 49 | /* scrollbars: true,*/ 50 | /* mouseWheel: true,*/ 51 | /* interactiveScrollbars: true,*/ 52 | /* shrinkScrollbars: 'scale',*/ 53 | /* fadeScrollbars: true,*/ 54 | /* probeType:1,*/ 55 | }; 56 | 57 | if (this.iScrollClick()) { 58 | scrollOption.click = true; 59 | } 60 | 61 | const scroll = new IScroll(elem, scrollOption); 62 | 63 | scroll.on('scrollEnd', ()=> { 64 | self._handleScrollEnd(); 65 | }); 66 | this.scroll = scroll; 67 | this.scroll.scrollTo(0, 0); 68 | }; 69 | 70 | _handleScrollEnd = ()=> { 71 | const { onScrollEnd } = this.props; 72 | if (typeof onScrollEnd === 'function') { 73 | onScrollEnd(this.scroll); 74 | } 75 | }; 76 | 77 | iScrollClick = ()=> { 78 | if (/iPhone|iPad|iPod|Macintosh/i.test(navigator.userAgent)) return false; 79 | if (/Chrome/i.test(navigator.userAgent)) return (/Android/i.test(navigator.userAgent)); 80 | if (/Silk/i.test(navigator.userAgent)) return false; 81 | if (/Android/i.test(navigator.userAgent)) { 82 | const s = navigator.userAgent.substr(navigator.userAgent.indexOf('Android') + 8, 3); 83 | return parseFloat(s[0] + s[3]) >= 44; 84 | } 85 | }; 86 | 87 | componentWillUnmout = ()=> { 88 | this.scroll.destroy(); 89 | }; 90 | 91 | render() { 92 | const { children } = this.props; 93 | let {} = this.state.data.toJS(); 94 | 95 | return ( 96 |
97 | {children} 98 |
99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /js/components/common/Scroll.scss: -------------------------------------------------------------------------------- 1 | .ScrollWrap{ 2 | position: relative; 3 | flex: 1; 4 | display: flex; 5 | flex-direction: column; 6 | overflow: hidden; 7 | } 8 | 9 | .Scroll{ 10 | position: absolute; 11 | top: 0; 12 | bottom: 0; 13 | left: 0; 14 | right: 0; 15 | flex:1; 16 | } 17 | -------------------------------------------------------------------------------- /js/components/form/InputQuantity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 9/14/2015. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { Map } from 'immutable'; 8 | 9 | import RegExes from '../../utils/RegExes'; 10 | import './InputQuantity.scss'; 11 | export default class InputQuantity extends React.Component { 12 | 13 | static propTypes = { 14 | min: PropTypes.number, 15 | value: PropTypes.number, 16 | onQuantityChange: PropTypes.func, 17 | }; 18 | 19 | static contextTypes = { 20 | router: PropTypes.object.isRequired, 21 | }; 22 | 23 | static defaultProps = { 24 | min: 0, 25 | value: 1, 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | data: Map({ 32 | value: props.value, 33 | min: props.min, 34 | }), 35 | }; 36 | } 37 | 38 | setImmState = (fn)=> 39 | this.setState(({ data }) => ({ 40 | data: fn(data), 41 | })); 42 | 43 | _handleMinus = ()=> { 44 | const { onQuantityChange } = this.props; 45 | const { value, min } = this.state.data.toJS(); 46 | 47 | if (min < value) { 48 | this.setImmState(d => d.set('value', parseInt(value - 1, 10))); 49 | if (typeof onQuantityChange === 'function') { 50 | onQuantityChange(parseInt(value - 1, 10)); 51 | } 52 | } 53 | }; 54 | 55 | _handleAdd = ()=> { 56 | const { onQuantityChange } = this.props; 57 | const { value } = this.state.data.toJS(); 58 | this.setImmState(d => d.set('value', parseInt(value + 1, 10))); 59 | if (typeof onQuantityChange === 'function') { 60 | onQuantityChange(parseInt(value + 1, 10)); 61 | } 62 | }; 63 | 64 | _handleChange = (event)=> { 65 | const value = event.target.value; 66 | 67 | if (RegExes.POSITIVE_INTEGER.test(value)) { 68 | this.setImmState(d => d.set('value', parseInt(value, 10))); 69 | 70 | const { onQuantityChange } = this.props; 71 | if (typeof onQuantityChange === 'function') { 72 | onQuantityChange(parseInt(value, 10)); 73 | } 74 | } else if (value === '') { 75 | this.setImmState(d => d.set('value', value)); 76 | } 77 | }; 78 | 79 | render() { 80 | const {} = this.props; 81 | const { value } = this.state.data.toJS(); 82 | 83 | return ( 84 |
85 | 86 | 87 | 88 |
89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /js/components/form/InputQuantity.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/variables"; 2 | 3 | .Input-quantity{ 4 | display: flex; 5 | align-items: center; 6 | 7 | .icon{ 8 | font-size: .6rem; 9 | color: #e76e41; 10 | 11 | &:first-child{ 12 | margin-right: .24rem; 13 | } 14 | 15 | &:last-child{ 16 | margin-left: .24rem; 17 | } 18 | } 19 | 20 | .input-text{ 21 | width: 1rem; 22 | height: .6rem; 23 | border: 1px solid $borderColor; 24 | text-align: center; 25 | } 26 | 27 | .unit{ 28 | margin-left:.32rem; 29 | } 30 | } -------------------------------------------------------------------------------- /js/components/product/ProductEntry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/28/16. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Relay from 'react-relay'; 8 | import { Map } from 'immutable'; 9 | import classNames from 'classnames'; 10 | 11 | import Price from '../common/Price'; 12 | 13 | import './ProductEntry.scss'; 14 | 15 | 16 | class ProductEntry extends React.Component { 17 | 18 | static propTypes = { 19 | product: PropTypes.object, 20 | children: PropTypes.object, 21 | onAddToCartClick: PropTypes.func, 22 | }; 23 | 24 | static contextTypes = { 25 | router: PropTypes.object.isRequired, 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | data: Map({}), 32 | }; 33 | } 34 | 35 | setImmState = (fn)=> 36 | this.setState(({ data }) => ({ 37 | data: fn(data), 38 | })); 39 | 40 | render() { 41 | const { product, children } = this.props; 42 | const {} = this.state.data.toJS(); 43 | 44 | return ( 45 |
46 | 47 | 48 | 49 |
50 |
{product.name}
51 | 52 | { children } 53 |
54 |
55 | ); 56 | } 57 | } 58 | 59 | 60 | export default Relay.createContainer(ProductEntry, { 61 | 62 | fragments: { 63 | product: () => Relay.QL` 64 | fragment on Product { 65 | id 66 | name 67 | price 68 | images(format: "thumbnail"){ 69 | url 70 | } 71 | } 72 | `, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /js/components/product/ProductEntry.scss: -------------------------------------------------------------------------------- 1 | .ProductEntry { 2 | 3 | .operation { 4 | .icon { 5 | font-size: .6rem; 6 | color: #e76e41; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /js/components/product/ProductList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Relay from 'react-relay'; 8 | import { Map } from 'immutable'; 9 | import debounce from 'lodash/debounce'; 10 | 11 | import Scroll from '../common/Scroll'; 12 | 13 | import CartWidget from '../cart/CartWidget'; 14 | import ProductEntry from './ProductEntry'; 15 | 16 | import AddToCartMutation from '../../mutations/AddToCartMutation'; 17 | 18 | const SIZE_PER_PAGE = 8; // 8 products per page 19 | 20 | import './ProductList.scss'; 21 | 22 | class ProductList extends React.Component { 23 | 24 | static propTypes = { 25 | products: PropTypes.object, 26 | cart: PropTypes.object, 27 | relay: PropTypes.object, 28 | }; 29 | 30 | static contextTypes = { 31 | router: PropTypes.object.isRequired, 32 | }; 33 | 34 | constructor(props) { 35 | super(props); 36 | this.state = { 37 | data: Map({ 38 | isLoading: false, 39 | offsetTopToLoadNextPage: 0, 40 | size: SIZE_PER_PAGE, 41 | }), 42 | }; 43 | } 44 | 45 | setImmState = (fn)=> 46 | this.setState(({ data }) => ({ 47 | data: fn(data), 48 | })); 49 | 50 | loadNextPage = ()=> { 51 | let { size } = this.state.data.toJS(); 52 | this.setImmState(d => d.set('isLoading', true)); 53 | const self = this; 54 | size = size + SIZE_PER_PAGE; 55 | 56 | this.props.relay.setVariables({ size }, (readyState)=> { 57 | if (readyState.ready) { 58 | self.setImmState(d => d.set('size', size).set('isLoading', false)); 59 | } 60 | }); 61 | }; 62 | 63 | 64 | /** 65 | * 66 | * @param target index of stepper 67 | */ 68 | animateCurveBall = (target)=> { 69 | // we need to get the start & end points before animate 70 | const startRect = target.getBoundingClientRect(); 71 | const startX = startRect.left + (startRect.width / 2); 72 | const startY = startRect.top + (startRect.height / 2); 73 | 74 | const duration = 800; // duration of animation, the smaller the faster 75 | 76 | // let's move the ball 77 | this.cartWidget.animateCurveBall(startX, startY, duration); 78 | }; 79 | 80 | addToCartDebounce = debounce(()=> { 81 | this.addToCartTransaction.commit(); 82 | this.addToCartTransaction = null; 83 | }, 500); 84 | 85 | addToCart = (product, quantity)=> { 86 | const { relay, viewer } = this.props; 87 | const { cart } = viewer; 88 | return relay.applyUpdate(new AddToCartMutation({ cart, product, quantity }), { 89 | onSuccess: () => { 90 | console.log('added to cart!'); 91 | }, 92 | onFailure: async(tansition) => { 93 | let errors; 94 | if (tansition.getError().source) { 95 | errors = tansition.getError() && tansition.getError().source.errors; 96 | } else { 97 | errors = (await tansition.getError().json()).errors; 98 | } 99 | errors.forEach((error)=> { 100 | Toast.fail(error.message, 10); 101 | }); 102 | }, 103 | }); 104 | }; 105 | 106 | /** 107 | * 108 | * @param product 109 | * @param quantity 110 | * @param event 111 | */ 112 | handleAddToCartClick = async(product, quantity, event)=> { 113 | this.animateCurveBall(event.target); 114 | 115 | if (this.addToCartTransaction) { 116 | await this.addToCartTransaction.rollback(); 117 | } 118 | 119 | this.addToCartTransaction = await this.addToCart(product, quantity); 120 | this.addToCartDebounce(); 121 | }; 122 | 123 | handleCartWidgetClick = ()=> { 124 | const { router } = this.context; 125 | router.push({ pathname: '/cart' }); 126 | }; 127 | 128 | handleScrollEnd = (event)=> { 129 | const { viewer } = this.props; 130 | const { products } = viewer; 131 | const { isLoading } = this.state.data.toJS(); 132 | 133 | if (!isLoading) { 134 | const lastThirdItemDOM = [].slice.call(event.scroller.children, -3); 135 | if (lastThirdItemDOM.length > 0) { 136 | /* load next page once scroll to the last three item */ 137 | const scrolledOffsetTop = Math.abs(event.y) + event.wrapperHeight + lastThirdItemDOM[0].offsetHeight; 138 | const offsetTopToLoadNextPage = lastThirdItemDOM[0].offsetTop; 139 | if (scrolledOffsetTop > offsetTopToLoadNextPage && products.pageInfo.hasNextPage) { 140 | this.loadNextPage(); 141 | } 142 | } 143 | } 144 | }; 145 | 146 | render() { 147 | const { viewer } = this.props; 148 | const { products, cart } = viewer; 149 | 150 | const entries = products.edges.map(({ node: product })=> { 151 | const cartEntryEdge = cart.entries.edges.find(({ node: entry })=>entry.product.id === product.id); 152 | const cartQuantity = cartEntryEdge ? cartEntryEdge.node.quantity : 0; 153 | 154 | return ( 155 |
156 | 158 | { 159 | cartQuantity > 0 && 160 |
161 |
162 |
{ cartQuantity }
163 |
164 | } 165 |
166 |
); 167 | }); 168 | 169 | return ( 170 |
171 | 172 |
173 |
174 |

{products.totalNumberOfItems} results.

175 |
176 | {entries} 177 |
178 | 179 | { /* show cart widget*/ } 180 | { this.cartWidget = c; }} 182 | onClick={this.handleCartWidgetClick} /> 183 |
184 |
185 | ); 186 | } 187 | } 188 | 189 | export default Relay.createContainer(ProductList, { 190 | 191 | initialVariables: { 192 | size: SIZE_PER_PAGE, 193 | }, 194 | 195 | fragments: { 196 | viewer: () => Relay.QL` 197 | fragment on Viewer{ 198 | id 199 | products(first: $size){ 200 | edges { 201 | node { 202 | id 203 | ${ProductEntry.getFragment('product')} 204 | ${AddToCartMutation.getFragment('product')} 205 | } 206 | } 207 | totalNumberOfItems 208 | pageInfo { 209 | hasNextPage 210 | hasPreviousPage 211 | } 212 | } 213 | cart { 214 | id 215 | entries(first: 100){ 216 | edges { 217 | node { 218 | id 219 | product{ 220 | id 221 | } 222 | quantity 223 | } 224 | } 225 | pageInfo { 226 | hasNextPage 227 | hasPreviousPage 228 | } 229 | } 230 | totalNumberOfItems 231 | ${AddToCartMutation.getFragment('cart')} 232 | } 233 | } 234 | `, 235 | }, 236 | }); 237 | -------------------------------------------------------------------------------- /js/components/product/ProductList.scss: -------------------------------------------------------------------------------- 1 | .ProductList { 2 | border-top: 1px solid #cfcfd2; 3 | background: #fff; 4 | 5 | .entries{ 6 | padding-right: .32rem; 7 | padding-left: .32rem; 8 | 9 | 10 | .info{ 11 | .cart-quantity{ 12 | display: flex; 13 | align-items: center; 14 | margin-left: .16rem; 15 | 16 | .quantity{ 17 | padding: .12rem .16rem; 18 | border-radius: .06rem; 19 | background: #e76e41; 20 | color: #fff; 21 | } 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /js/components/styles/animations.scss: -------------------------------------------------------------------------------- 1 | @import "./animations/fade"; 2 | @import "./animations/slide"; 3 | @import "./animations/shake"; 4 | @import "./animations/scale"; -------------------------------------------------------------------------------- /js/components/styles/animations/fade.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeIn { 2 | from { 3 | opacity: 0; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | @keyframes fadeInOut { 11 | 0%, 100% { 12 | opacity: 0; 13 | } 14 | 15 | 40%, 60% { 16 | opacity: 1; 17 | } 18 | 19 | } 20 | 21 | .fade-in-out { 22 | opacity: 0; 23 | animation: fadeInOut ease-in-out 1; 24 | animation-fill-mode: both; 25 | animation-duration: 1s; 26 | 27 | } 28 | 29 | .fade-in { 30 | opacity: 0; /* make things invisible upon start */ 31 | animation: fadeIn ease-in 1; 32 | animation-fill-mode: forwards; 33 | animation-duration: 1s; 34 | 35 | &.fast { 36 | animation-duration: 300ms; 37 | } 38 | } -------------------------------------------------------------------------------- /js/components/styles/animations/scale.scss: -------------------------------------------------------------------------------- 1 | @keyframes scale { 2 | from { 3 | transform: scale(1); 4 | } 5 | 6 | to{ 7 | transform: scale(1.4); 8 | } 9 | } 10 | 11 | .scale{ 12 | animation: scale ease-in-out 1; 13 | animation-fill-mode: backwards; 14 | animation-duration: .3s; 15 | } -------------------------------------------------------------------------------- /js/components/styles/animations/shake.scss: -------------------------------------------------------------------------------- 1 | @keyframes shake { 2 | 0% { 3 | transform: translateX(0px); 4 | } 5 | 20% { 6 | transform: translateX(8px); 7 | } 8 | 40% { 9 | transform: translateX(-8px); 10 | } 11 | 60% { 12 | transform: translateX(8px); 13 | } 14 | 80% { 15 | transform: translateX(-8px); 16 | } 17 | 100% { 18 | transform: translateX(0px); 19 | } 20 | } 21 | 22 | .shake { 23 | animation: shake ease-in 1; 24 | animation-fill-mode: forwards; 25 | animation-duration: .6s; 26 | } -------------------------------------------------------------------------------- /js/components/styles/animations/slide.scss: -------------------------------------------------------------------------------- 1 | 2 | @keyframes slideFromRight { 3 | from { 4 | transform: translate3d(100%, 0, 0); 5 | } 6 | 7 | to { 8 | transform: translate3d(0, 0, 0); 9 | } 10 | } 11 | 12 | .slide-from-right { 13 | animation: slideFromRight ease-in 1; 14 | animation-fill-mode: forwards; 15 | animation-duration: .3s; 16 | } 17 | 18 | @keyframes collapseToRight { 19 | from { 20 | transform: translate3d(0, 0, 0); 21 | } 22 | 23 | to { 24 | transform: translate3d(100%, 0, 0); 25 | } 26 | } 27 | 28 | .collapse-to-right { 29 | animation: collapseToRight ease-in 1 !important; 30 | animation-fill-mode: forwards !important; 31 | animation-duration: .3s !important; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /js/components/styles/common.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | label, input { 4 | display: flex; 5 | } 6 | 7 | body { 8 | background: $backgroundColor; 9 | color: #333; 10 | font-size: 14px; 11 | font-family: sans-serif; 12 | -webkit-overflow-scrolling: touch; 13 | } 14 | 15 | html, body { 16 | width: 100%; 17 | height: 100%; 18 | background-color: $backgroundColor; 19 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 20 | -webkit-text-size-adjust: none; 21 | font-family: sans-serif; 22 | } 23 | 24 | ol, ul { 25 | list-style: none; 26 | } 27 | 28 | ins, a { 29 | text-decoration: none; 30 | } 31 | 32 | a:hover { 33 | text-decoration: none; 34 | } 35 | 36 | a, a:visited { 37 | color: inherit; 38 | } 39 | 40 | input, select, textarea { 41 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 42 | -webkit-appearance: none; 43 | border: 0; 44 | border-radius: 0; 45 | } 46 | 47 | /*utils*/ 48 | .clearfix:before, 49 | .clearfix:after { 50 | content: " "; /* 1 */ 51 | display: table; /* 2 */ 52 | } 53 | 54 | .clearfix:after { 55 | clear: both; 56 | } 57 | 58 | .center { 59 | text-align: center; 60 | } 61 | 62 | .hide { 63 | display: none !important; 64 | } 65 | 66 | .invisible { 67 | visibility: hidden; 68 | } 69 | 70 | .block { 71 | display: block; 72 | } 73 | 74 | .horizontal { 75 | display: flex; 76 | align-items: center; 77 | } 78 | 79 | .highlight { 80 | color: $fontHighLightColor !important; 81 | } 82 | 83 | .light { 84 | color: $fontLightColor; 85 | } 86 | 87 | .price { 88 | color: $fontHighLightColor; 89 | } 90 | 91 | .gap { 92 | flex: 1; 93 | width: 100%; 94 | padding-top: .32rem; 95 | } 96 | 97 | #root { 98 | display: flex; 99 | flex-direction: column; 100 | height: 100%; 101 | width: 100%; 102 | } 103 | 104 | .centre { 105 | flex: 1; 106 | display: flex; 107 | flex-direction: column; 108 | align-items: center; 109 | justify-content: center; 110 | 111 | .centre-content { 112 | flex: 1; 113 | display: flex; 114 | flex-direction: column; 115 | align-items: center; 116 | justify-content: center; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /js/components/styles/entries.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .entries { 4 | 5 | > .description, .status { 6 | padding-bottom: .32rem; 7 | padding-top: .32rem; 8 | } 9 | 10 | > .description { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | } 15 | 16 | .status { 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | flex-direction: column; 21 | border-top: 1px solid $borderColor; 22 | padding-bottom: .32rem; 23 | } 24 | 25 | .entry { 26 | display: flex; 27 | align-items: center; 28 | padding-bottom: .32rem; 29 | padding-top: .32rem; 30 | border-top: 1px solid $borderColor; 31 | 32 | &.borderless { 33 | border-top: 0; 34 | } 35 | 36 | > .Checkbox, > .Icon { 37 | &:first-of-type { 38 | margin-right: .32rem; 39 | color: $fontHighLightColor; 40 | } 41 | } 42 | 43 | .image { 44 | margin-right: .32rem; 45 | height: 1.28rem; 46 | line-height: 1.28rem; 47 | width: 1.28rem; 48 | text-align: center; 49 | border: 1px solid $borderColor; 50 | } 51 | 52 | .info { 53 | flex: 1; 54 | 55 | .description { 56 | margin-top: 0; 57 | padding: 0; 58 | } 59 | 60 | .price, .operation { 61 | margin-top: .16rem; 62 | } 63 | 64 | .operation{ 65 | display: flex; 66 | } 67 | } 68 | 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /js/components/styles/icons.scss: -------------------------------------------------------------------------------- 1 | 2 | .icon{ 3 | display: inline-block; 4 | } 5 | -------------------------------------------------------------------------------- /js/components/styles/shape.scss: -------------------------------------------------------------------------------- 1 | 2 | .circle { 3 | width: 100px; 4 | height: 100px; 5 | background: red; 6 | border-radius: 50px; 7 | } 8 | 9 | .close-circled { 10 | width: 100px; 11 | height: 100px; 12 | background: red; 13 | border-radius: 50px; 14 | 15 | &:before, &:after { 16 | content: ''; 17 | position: absolute; 18 | top: 50%; 19 | left: 50%; 20 | width: 60%; 21 | transform: translate(-50%, -50%) rotate(45deg); 22 | background-color: #fff; 23 | border: .2em solid #eee; 24 | border-bottom: transparent; 25 | border-left: transparent; 26 | border-radius: .2em; 27 | } 28 | 29 | &:after { 30 | transform: translate(-50%, -50%) rotate(-45deg); 31 | } 32 | } 33 | 34 | .circle-filled { 35 | float: left; 36 | width: 0.26rem; 37 | height: 0.26rem; 38 | margin-right: 0.26rem; 39 | background-color: #c9c9c9; 40 | border: 0.06rem solid #e7e6e5; 41 | border-radius: 0.26rem; 42 | } 43 | 44 | .arrow-left { 45 | width: 0; 46 | height: 0; 47 | border-top: .16rem solid transparent; 48 | border-bottom: .16rem solid transparent; 49 | border-right: .16rem solid #e76e41; 50 | } -------------------------------------------------------------------------------- /js/components/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $lightGray:#cfcfd2; 2 | $gray:#999; 3 | $orangered:#e76e41; 4 | 5 | $borderColor:#cfcfd2; 6 | $fontColor:#333; 7 | $fontLightColor:#999; 8 | $fontHighLightColor: #e76e41; 9 | $backgroundColor:#f0f0f0; -------------------------------------------------------------------------------- /js/mutations/AddToCartMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/28/16. 3 | */ 4 | 5 | import Relay from 'react-relay'; 6 | 7 | export default class AddToCartMutation extends Relay.Mutation { 8 | static fragments = { 9 | product: () => Relay.QL` 10 | fragment on Product { 11 | id 12 | price 13 | } 14 | `, 15 | cart: () => Relay.QL` 16 | fragment on Cart { 17 | id 18 | entries(first: 100){ 19 | edges { 20 | node { 21 | id 22 | product{ 23 | id 24 | } 25 | quantity 26 | } 27 | } 28 | pageInfo { 29 | hasNextPage 30 | hasPreviousPage 31 | } 32 | } 33 | totalNumberOfItems 34 | totalPriceOfItems 35 | } 36 | `, 37 | }; 38 | 39 | getMutation() { 40 | return Relay.QL`mutation{addToCart}`; 41 | } 42 | 43 | getFatQuery() { 44 | return Relay.QL` 45 | fragment on AddToCartPayload { 46 | cartEntryEdge 47 | cart{ 48 | id 49 | entries 50 | totalNumberOfItems 51 | totalPriceOfItems 52 | } 53 | cartEntry{ 54 | id 55 | quantity 56 | } 57 | } 58 | `; 59 | } 60 | 61 | getConfigs() { 62 | const { cart, product } = this.props; 63 | const configs = [{ 64 | type: 'FIELDS_CHANGE', 65 | fieldIDs: { 66 | cart: cart.id, 67 | }, 68 | }]; 69 | 70 | const cartEntryEdge = cart.entries.edges.find(({ node: en })=>en.product.id === product.id); 71 | 72 | if (!cartEntryEdge) { 73 | configs.push({ 74 | type: 'RANGE_ADD', 75 | parentName: 'cart', 76 | parentID: cart.id, 77 | connectionName: 'entries', 78 | edgeName: 'cartEntryEdge', 79 | rangeBehaviors: { 80 | '': 'prepend', 81 | }, 82 | }, { 83 | type: 'REQUIRED_CHILDREN', 84 | // Forces these fragments to be included in the query 85 | children: [Relay.QL` 86 | fragment on AddToCartPayload { 87 | cartEntryEdge 88 | } 89 | `], 90 | }); 91 | } else { 92 | configs.push({ 93 | type: 'FIELDS_CHANGE', 94 | fieldIDs: { 95 | cart: cart.id, 96 | cartEntry: cartEntryEdge.node.id, 97 | }, 98 | }); 99 | } 100 | 101 | return configs; 102 | // return [{ 103 | // type: 'FIELDS_CHANGE', 104 | // fieldIDs: { 105 | // cart: this.props.cart.id, 106 | // }, 107 | // }, { 108 | // type: 'RANGE_ADD', 109 | // parentName: 'cart', 110 | // parentID: this.props.cart.id, 111 | // connectionName: 'entries', 112 | // edgeName: 'cartEntryEdge', 113 | // rangeBehaviors: { 114 | // '': 'append', 115 | // }, 116 | // }]; 117 | } 118 | 119 | getVariables() { 120 | return { 121 | id: this.props.product.id, 122 | quantity: this.props.quantity, 123 | }; 124 | } 125 | 126 | getOptimisticResponse() { 127 | const { product, cart, quantity } = this.props; 128 | 129 | const cartPayload = { 130 | id: cart.id, 131 | totalNumberOfItems: cart.totalNumberOfItems, 132 | totalPriceOfItems: cart.totalPriceOfItems, 133 | }; 134 | 135 | let cartEntryPayload; 136 | 137 | let cartEntryEdge = cart.entries.edges.find(({ node: entry })=>entry.product.id === product.id); 138 | 139 | if (cartEntryEdge) { 140 | cartEntryPayload = cartEntryEdge.node; 141 | const marginQuantity = quantity - cartEntryPayload.quantity; 142 | cartPayload.totalNumberOfItems += marginQuantity; 143 | cartEntryPayload.quantity = quantity; 144 | cartPayload.totalPriceOfItems += (marginQuantity * product.price); 145 | } else { 146 | cartEntryPayload = { 147 | quantity, 148 | product, 149 | }; 150 | cartEntryEdge = { 151 | node: cartEntryPayload, 152 | }; 153 | cartPayload.totalNumberOfItems += quantity; 154 | cartPayload.totalPriceOfItems += (quantity * product.price); 155 | } 156 | 157 | return { 158 | cartPayload, 159 | cartEntryEdge, 160 | cartEntryPayload 161 | }; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /js/mutations/RemoveFromCartMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 3/1/16. 3 | */ 4 | 5 | import Relay from 'react-relay'; 6 | 7 | export default class RemoveFromCartMutation extends Relay.Mutation { 8 | static fragments = { 9 | cartEntry: () => Relay.QL` 10 | fragment on CartEntry { 11 | id 12 | quantity 13 | price 14 | totalPrice 15 | } 16 | `, 17 | cart: () => Relay.QL` 18 | fragment on Cart { 19 | id 20 | totalNumberOfItems 21 | totalPriceOfItems 22 | } 23 | `, 24 | }; 25 | 26 | getMutation() { 27 | return Relay.QL`mutation{removeFromCart}`; 28 | } 29 | 30 | getFatQuery() { 31 | return Relay.QL` 32 | fragment on RemoveFromCartPayload { 33 | deletedCartEntryId 34 | cart{ 35 | id 36 | totalNumberOfItems 37 | totalPriceOfItems 38 | } 39 | } 40 | `; 41 | } 42 | 43 | getConfigs() { 44 | return [{ 45 | type: 'NODE_DELETE', 46 | parentName: 'cart', 47 | parentID: this.props.cart.id, 48 | connectionName: 'entries', 49 | deletedIDFieldName: 'deletedCartEntryId', 50 | }]; 51 | } 52 | 53 | getVariables() { 54 | return { 55 | id: this.props.cartEntry.id, 56 | }; 57 | } 58 | 59 | getOptimisticResponse() { 60 | const { cart, cartEntry } = this.props; 61 | 62 | const cartPayload = { 63 | totalNumberOfItems: cart.totalNumberOfItems - cartEntry.quantity, 64 | totalPriceOfItems: cart.totalPriceOfItems - cartEntry.totalPrice, 65 | }; 66 | 67 | return { 68 | deletedCartEntryId: this.props.cartEntry.id, 69 | cart: cartPayload, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /js/queries/viewerQueries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | import Relay from 'react-relay'; 5 | export default { 6 | viewer: () => Relay.QL` query { viewer } `, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /js/routes/cartRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/26/16. 3 | */ 4 | export default { 5 | path: 'cart', 6 | getComponent(nextState, cb) { 7 | const self = this; 8 | require.ensure([], (require) => { 9 | self.queries = require('../queries/viewerQueries').default; 10 | const component = require('../components/cart/Cart').default; 11 | 12 | self.component = component; 13 | cb(null, component); 14 | }); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /js/routes/productListRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/23/16. 3 | */ 4 | export default { 5 | path: 'product-list', 6 | getComponent(nextState, cb) { 7 | const self = this; 8 | require.ensure([], (require) => { 9 | self.queries = require('../queries/viewerQueries').default; 10 | const component = require('../components/product/ProductList').default; 11 | 12 | self.component = component; 13 | self.queryParams = ['categoryCode', 'text']; 14 | cb(null, component); 15 | }); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /js/routes/rootRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | const rootRoute = [{ 5 | path: '/', 6 | component: require('../components/App').default, 7 | childRoutes: [ 8 | require('./cartRoute').default, 9 | require('./productListRoute').default, 10 | ], 11 | }]; 12 | 13 | export default rootRoute; 14 | 15 | const css = 'opacity:0;font-size:24px;color:#fff;text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 10px 10px rgba(0,0,0,.2), 0 20px 20px rgba(0,0,0,.15);'; 16 | console.log('%cSGFpbCBSZWFjdCAmIFJlbGF5ISBzb29uQGxpdmUuY24=', css); 17 | -------------------------------------------------------------------------------- /js/utils/RegExes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 9/2/2015. 3 | */ 4 | export default class RegExes { 5 | static POSITIVE_INTEGER = /^[1-9]\d*$/; 6 | static MATRIX_VALUE = /matrix(?:(3d)\(-{0,1}\d+(?:, -{0,1}\d+)*(?:, (-{0,1}\d+))(?:, (-{0,1}\d+))(?:, (-{0,1}\d+)), -{0,1}\d+\)|\(-{0,1}\d+(?:, -{0,1}\d+)*(?:, (-{0,1}\d+))(?:, (-{0,1}\d+))\))/; 7 | static NON_NULL = /^[^\s]*$/g; 8 | } 9 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 7/24/2015. 3 | */ 4 | import winston from 'winston'; 5 | import moment from 'moment'; 6 | 7 | winston.emitErrs = true; 8 | 9 | const logger = new winston.Logger({ 10 | transports: [ 11 | new winston.transports.File({ 12 | level: 'debug', 13 | filename: './logs/all-logs.log', 14 | handleExceptions: true, 15 | maxsize: 5242880, // 5MB 16 | maxFiles: 5, 17 | colorize: false, 18 | json: false, 19 | prettyPrint: true, 20 | timestamp: ()=> moment().format('YYYY-MM-DD HH:mm Z'), 21 | }), 22 | new winston.transports.Console({ 23 | level: 'debug', 24 | handleExceptions: true, 25 | json: false, 26 | colorize: true, 27 | prettyPrint: true, 28 | }), 29 | ], 30 | exitOnError: false, 31 | }); 32 | 33 | logger.stream = { 34 | write: (message)=> { 35 | logger.info(message); 36 | }, 37 | }; 38 | 39 | export default logger; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-cart", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "babel-node ./server.js", 7 | "updateSchema": "babel-node ./scripts/updateSchema.js", 8 | "webpack": "webpack --config webpack.dev.config.js" 9 | }, 10 | "dependencies": { 11 | "classnames": "2.2.5", 12 | "cookie-parser": "1.4.3", 13 | "express": "4.15.2", 14 | "express-graphql": "0.6.4", 15 | "express-session": "1.15.2", 16 | "fastclick": "1.0.6", 17 | "graphql": "0.9.2", 18 | "graphql-relay": "0.5.1", 19 | "immutable": "3.8.1", 20 | "ionicons": "3.0.0", 21 | "iscroll": "5.2.0", 22 | "lodash": "4.17.4", 23 | "moment": "2.18.1", 24 | "morgan": "1.8.1", 25 | "normalize.css": "6.0.0", 26 | "prop-types": "^15.5.8", 27 | "react": "15.5.4", 28 | "react-dom": "15.5.4", 29 | "react-relay": "0.10.0", 30 | "react-router": "3.0.5", 31 | "react-router-relay": "0.13.7", 32 | "session-memory-store": "0.2.2", 33 | "webpack": "^2.4.1", 34 | "webpack-dev-server": "2.4.2", 35 | "winston": "2.3.1" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "6.7.7", 39 | "babel-cli": "^6.24.1", 40 | "babel-core": "6.24.1", 41 | "babel-loader": "6.4.1", 42 | "babel-plugin-transform-class-properties": "^6.24.1", 43 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 44 | "babel-plugin-transform-regenerator": "^6.24.1", 45 | "babel-plugin-transform-runtime": "^6.23.0", 46 | "babel-polyfill": "6.23.0", 47 | "babel-preset-es2015": "^6.24.1", 48 | "babel-preset-es2015-loose": "^8.0.0", 49 | "babel-preset-es2017": "^6.24.1", 50 | "babel-preset-react": "6.24.1", 51 | "babel-preset-stage-0": "6.24.1", 52 | "babel-register": "^6.24.1", 53 | "babel-relay-plugin": "0.11.0", 54 | "babel-runtime": "^6.23.0", 55 | "css-loader": "0.28.0", 56 | "del": "2.2.2", 57 | "eslint": "^3.19.0", 58 | "eslint-config-airbnb": "^14.1.0", 59 | "eslint-plugin-import": "^2.2.0", 60 | "eslint-plugin-jsx-a11y": "^4.0.0", 61 | "eslint-plugin-react": "^6.10.3", 62 | "extract-text-webpack-plugin": "2.1.0", 63 | "file-loader": "0.11.1", 64 | "gulp": "3.9.1", 65 | "gulp-rename": "1.2.2", 66 | "gulp-replace": "0.5.4", 67 | "gulp-util": "3.0.8", 68 | "node-sass": "4.5.2", 69 | "postcss-loader": "1.3.3", 70 | "require-dir": "0.3.1", 71 | "sass-loader": "6.0.3", 72 | "style-loader": "0.16.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Xin on 15/12/2016. 3 | */ 4 | module.exports = { 5 | module: { 6 | loaders: [ 7 | { 8 | test: '\/.css', 9 | loaders: [ 10 | 'style-loader', 11 | 'css-loader?importLoaders=1', 12 | 'postcss-loader?sourceMap=inline' 13 | ] 14 | } 15 | ] 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | relay-cart 7 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/updateSchema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env babel-node --optional es7.asyncFunctions 2 | /** 3 | * Copyright 2013-2015, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | import fs from 'fs'; 12 | import path from 'path'; 13 | import Schema from '../data/schema'; 14 | import { graphql } from 'graphql'; 15 | import { introspectionQuery, printSchema } from 'graphql/utilities'; 16 | 17 | // Save JSON of full schema introspection for Babel Relay Plugin to use 18 | (async () => { 19 | const result = await (graphql(Schema, introspectionQuery)); 20 | 21 | if (result.errors) { 22 | console.error( 23 | 'ERROR introspecting schema: ', 24 | JSON.stringify(result.errors, null, 2) 25 | ); 26 | } else { 27 | console.log(path.join(__dirname, '../data/schema.json')); 28 | fs.writeFileSync( 29 | path.join(__dirname, '../data/schema.json'), 30 | JSON.stringify(result, null, 2) 31 | ); 32 | } 33 | })(); 34 | 35 | // Save user readable type system shorthand of schema 36 | fs.writeFileSync( 37 | path.join(__dirname, '../data/schema.graphqls'), 38 | printSchema(Schema) 39 | ); 40 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | import cookieParser from 'cookie-parser'; 5 | import express from 'express'; 6 | import graphQLHTTP from 'express-graphql'; 7 | import morgan from 'morgan'; 8 | import path from 'path'; 9 | import session from 'express-session'; 10 | import webpack from 'webpack'; 11 | import WebpackDevServer from 'webpack-dev-server'; 12 | import pkg from './package.json'; 13 | import { Schema } from './data/schema'; 14 | import webpackConfig from './webpack.dev.config'; 15 | import logger from './logger'; 16 | 17 | const APP_PORT = 3000; 18 | const GRAPHQL_PORT = 8080; 19 | 20 | const rootPath = path.join(__dirname); 21 | const publicPath = path.join(rootPath, 'public'); 22 | 23 | const graphQLServer = express(); 24 | const MemoryStore = require('session-memory-store')(session); 25 | 26 | graphQLServer.use(cookieParser()); 27 | 28 | graphQLServer.use(session({ 29 | store: new MemoryStore(), 30 | secret: pkg.name, 31 | resave: false, 32 | saveUninitialized: true, 33 | })); 34 | 35 | graphQLServer.use('/graphql', 36 | graphQLHTTP(request => ({ 37 | schema: Schema, 38 | rootValue: request, 39 | pretty: true, 40 | graphiql: true, 41 | context: request.session, 42 | })), 43 | ); 44 | 45 | graphQLServer.listen(GRAPHQL_PORT, () => console.log( 46 | `GraphQL Server is now running on http://localhost:${GRAPHQL_PORT}`, 47 | )); 48 | 49 | // create a single instance of the compiler to allow caching 50 | const devCompiler = webpack(webpackConfig); 51 | 52 | // Start a webpack-dev-server 53 | const app = new WebpackDevServer(devCompiler, { 54 | publicPath: webpackConfig.output.publicPath, 55 | contentBase: path.join(__dirname, 'build', 'public'), 56 | hot: true, 57 | stats: { 58 | colors: true, 59 | hash: false, 60 | timings: true, 61 | assets: false, 62 | chunks: false, 63 | chunkModules: false, 64 | modules: false, 65 | children: false, 66 | }, 67 | historyApiFallback: true, 68 | compress: true, 69 | proxy: { '/graphql': `http://localhost:${GRAPHQL_PORT}` }, 70 | }); 71 | 72 | // Static files middleware 73 | app.use(express.static(publicPath)); 74 | 75 | app.use(morgan('combined', { stream: logger.stream })); 76 | 77 | app.listen(APP_PORT, 'localhost', () => { 78 | console.log(`App Server is now running on http://localhost:${APP_PORT}`); 79 | console.log('[webpack-dev-server]', `http://localhost:${APP_PORT}/webpack-dev-server/index.html`); 80 | }); 81 | -------------------------------------------------------------------------------- /tasks/dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 10/18/2015. 3 | */ 4 | import path from 'path'; 5 | import { exec } from 'child_process'; 6 | import gulp from 'gulp'; 7 | import gutil from 'gulp-util'; 8 | 9 | const rootPath = path.join(__dirname, '..'); 10 | 11 | gulp.task('dev:schema-generate', ()=> { 12 | exec(`babel-node ${rootPath}/scripts/updateSchema.js`); 13 | }); 14 | 15 | // recompile the schema whenever .js files in data are updated 16 | gulp.task('dev:schema-watch', () => { 17 | const watcher = gulp.watch([ 18 | path.join(rootPath, 'data', '**/*.js'), 19 | /* path.join(__dirname, 'src', 'types', '**!/!*.js'), 20 | path.join(__dirname, 'src', 'mutations', '**!/!*.js'),*/ 21 | ], ['dev:schema-generate']); 22 | watcher.on('change', (event)=> { 23 | gutil.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Soon on 2/22/16. 3 | */ 4 | const webpack = require('webpack'); 5 | const path = require('path'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | 8 | const rootPath = path.resolve('.'); 9 | const outputPath = path.join(rootPath, 'dist', 'dev', 'public'); 10 | 11 | const jsPath = path.join(rootPath, 'js'); 12 | 13 | const stylePaths = [ 14 | path.join(jsPath, 'components'), 15 | path.join(rootPath, 'node_modules', 'normalize.css'), 16 | path.join(rootPath, 'node_modules', 'ionicons', 'dist', 'scss'), 17 | ]; 18 | 19 | const fontPaths = [ 20 | path.join(rootPath, 'node_modules', 'ionicons', 'dist', 'fonts'), 21 | ]; 22 | 23 | 24 | module.exports = { 25 | // devtool: 'eval', 26 | devtool: 'source-map', 27 | 28 | entry: [ 29 | 'webpack-dev-server/client?http://localhost:3000', 30 | 'webpack/hot/dev-server', 31 | './js/app.js', 32 | ], 33 | 34 | output: { 35 | filename: '[name].js', 36 | path: outputPath, 37 | publicPath: '/public/', 38 | }, 39 | 40 | module: { 41 | loaders: [ 42 | { 43 | test: /\.js$/, 44 | use: 'babel-loader', 45 | exclude: /node_modules/, 46 | }, 47 | { 48 | test: /\.(css|scss)$/, 49 | use: ExtractTextPlugin.extract({ 50 | fallback: 'style-loader', 51 | use: [ 52 | 'css-loader', 53 | 'sass-loader?sourceMap=true', 54 | 'postcss-loader', 55 | ], 56 | }), 57 | include: stylePaths, 58 | }, 59 | { 60 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?.*$/, 61 | use: 'file-loader?limit=100000&minetype=application/font-woff', 62 | include: fontPaths, 63 | }, 64 | { 65 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?.*$/, 66 | use: 'file-loader', 67 | include: fontPaths, 68 | }, 69 | ], 70 | }, 71 | 72 | plugins: [ 73 | new ExtractTextPlugin({ filename: '[name].css', allChunks: true }), 74 | new webpack.HotModuleReplacementPlugin(), 75 | new webpack.DefinePlugin({ 76 | 'process.env': { 77 | NODE_ENV: JSON.stringify('development'), 78 | }, 79 | }), 80 | ], 81 | 82 | }; 83 | --------------------------------------------------------------------------------