├── .gitignore ├── README.md ├── configs └── formConfig.js ├── lerna.json ├── package.json ├── packages ├── CB-serverless-backend │ ├── .DS_Store │ ├── .babelrc │ ├── LICENSE │ ├── README.md │ ├── api │ │ ├── .DS_Store │ │ ├── cart │ │ │ ├── createCart.js │ │ │ ├── getCart.js │ │ │ └── getCartWithDetails.js │ │ ├── groceries │ │ │ ├── getGroceries.js │ │ │ ├── getGrocery.js │ │ │ └── stock.js │ │ ├── order │ │ │ ├── cancelOrder.js │ │ │ ├── createOrder.js │ │ │ └── getOrders.js │ │ ├── pay │ │ │ └── makePayment.js │ │ └── utils │ │ │ └── index.js │ ├── dynamoDb │ │ ├── awsConfigUpdate.js │ │ ├── constants.js │ │ ├── createTable.js │ │ ├── data │ │ │ ├── groceryList.js │ │ │ └── sampleCart.js │ │ ├── deleteTable.js │ │ └── populateTable.js │ ├── env.example │ ├── package-lock.json │ ├── package.json │ ├── serverless.yml │ ├── tests │ │ └── handler.test.js │ ├── utils │ │ ├── awsConfigUpdate.js │ │ ├── config.js │ │ ├── getErrorResponse.js │ │ ├── getSuccessResponse.js │ │ └── orderIdGenerator.js │ ├── webpack.config.js │ └── yarn.lock └── CB-serverless-frontend │ ├── .DS_Store │ ├── .eslintrc.json │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── Auth │ │ ├── ForgotPasswordForm.js │ │ ├── LoginForm.js │ │ ├── RegisterForm.js │ │ ├── VerificationForm.js │ │ ├── actionCreators.js │ │ ├── authReducer.js │ │ ├── common │ │ │ └── buttons.js │ │ ├── index.js │ │ └── styles.css │ ├── actions │ │ ├── cart.js │ │ ├── order.js │ │ └── payment.js │ ├── base_components │ │ ├── CartItemSkeleton.js │ │ ├── Footer.js │ │ ├── OrderButton.js │ │ ├── ProductImage.js │ │ ├── ProductSkeleton.js │ │ ├── Quantity.js │ │ └── index.js │ ├── components │ │ ├── Cart │ │ │ ├── BillReceipt.js │ │ │ ├── CartHome.js │ │ │ ├── CartItem.js │ │ │ └── styles │ │ │ │ └── components.js │ │ ├── Category │ │ │ ├── CategoryItems.js │ │ │ └── sub-categories.js │ │ ├── Product │ │ │ ├── ProductHome.js │ │ │ ├── ProductItem.js │ │ │ └── ProductRow.js │ │ ├── ProfileHome.js │ │ ├── header.js │ │ ├── order-list │ │ │ ├── details.js │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── order-placed.js │ ├── constants │ │ └── app.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reducers │ │ ├── cart.js │ │ ├── orders.js │ │ └── payment.js │ ├── registerServiceWorker.js │ ├── routes.js │ ├── sagas │ │ ├── auth │ │ │ ├── authenticationSaga.js │ │ │ ├── forgotPasswordRequestSaga.js │ │ │ ├── forgotPasswordSaga.js │ │ │ ├── loginFailureSaga.js │ │ │ ├── loginSaga.js │ │ │ ├── registerSaga.js │ │ │ ├── requestVerificationCodeSaga.js │ │ │ └── verifyUserSaga.js │ │ ├── cart │ │ │ ├── cartItemsAddSaga.js │ │ │ ├── cartItemsCleanSaga.js │ │ │ ├── cartItemsDeleteSaga.js │ │ │ ├── cartItemsFetchSaga.js │ │ │ └── cartItemsUpdateQtySaga.js │ │ ├── index.js │ │ ├── order │ │ │ ├── cancelOrderSaga.js │ │ │ ├── cleanOrderSaga.js │ │ │ ├── fetchAllOrdersSaga.js │ │ │ └── placeOrderSaga.js │ │ └── payment │ │ │ └── paymentTokenIdSubmitSaga.js │ ├── selectors │ │ ├── bill-receipt.js │ │ ├── cart-home.js │ │ ├── common │ │ │ ├── cart-data.js │ │ │ ├── cart-items-info.js │ │ │ ├── current-order.js │ │ │ └── user-data.js │ │ ├── header.js │ │ ├── order-list.js │ │ └── profile-home.js │ ├── service │ │ ├── api_constants.js │ │ ├── cart.js │ │ ├── grocery.js │ │ ├── order.js │ │ ├── payment.js │ │ └── request.js │ ├── store.js │ └── utils │ │ ├── array.js │ │ ├── string.js │ │ └── stripe-payment-modal.js │ └── yarn.lock ├── scripts ├── backendScripts.js ├── htmlInputs.js └── scaffold-form.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .idea/ 4 | packages/.DS_Store 5 | packages/CB-serverless-backend/.DS_Store 6 | packages/CB-serverless-frontend/.DS_Store 7 | packages/CB-serverless-frontend/.env 8 | packages/CB-serverless-backend/dynamoDb/shared-local-instance.db 9 | .webpack 10 | .serverless 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | App Demo https://media.giphy.com/media/1sw6syoMDm6QEu9Stk/giphy.gif 2 | 3 | **What this app about ?** 4 | 5 | This is a mock grocery purchase app built on the serverless technology. 6 | 7 | 1. Serverless Lambda Functions were written to serve as a backend REST APIs. 8 | 2. Front End is done with React which uses those APIs 9 | 10 | **Motivation** 11 | 12 | To explore the limit of serverless framework 13 | 14 | **Tech Stack** 15 | 16 | ![image](http://awscomputeblogmedia.s3.amazonaws.com/zombie_high_level_architecture_of_survivor_serverless_chat_app.png) 17 | 18 | **Functionalities** 19 | 20 | App will have users who can register / login 21 | 22 | 1. List of grocery items based on various categories (Eatable, drinkable, cookable, hyigene) 23 | 2. Add items to cart if that stock is present for a grocery item. 24 | 3. Checking out from a cart to place order 25 | 4. Payment for an order / Cancelling the order 26 | 27 | **To be built** 28 | 1. A global search bar for item search 29 | 2. Chat functionality between users and customer support (Admin of the app) 30 | 31 | **Prerequisites:** 32 | 33 | 1. Install AWS CLI 34 | 2. Add user based on AWS credentials which will be shared directly. 35 | 3. Auth will be currently based on AWS Cognito. Login / Registration will be handled by the front end auth module directly with the cognito. Lambda functions won't be responsible for auth. It requires an userId (AccessKeyId) based on which we will maintain the DB. 36 | 4. All other APIs will be working locally. 37 | 5. Front End would be developed locally and finally deployed on S3. 38 | 39 | **Accounts Required to deploy on cloud** 40 | 41 | 1. Cognito user pool to be created which will manage users in the app. You need to specify it on the front end as mentioned later in the readme. Also For backend refer ```To Setup Backend point 10 ``` 42 | 2. Deployment will use S3, API gateway and lambda functions along with DynamoDB. Ensure that you have access in your AWS on which it will be deployed 43 | 44 | **To setup Backend:** 45 | 1. Choose a region as per AWS (Our App is ap-south-1). 46 | 2. Check out ```utils/config.js``` and change the region as per that. Choose db url locally or accordingly on Cloud. For more details look at https://docs.aws.amazon.com/general/latest/gr/rande.html 47 | 3. Install AWS DynamoDB for this region locally and run the server which will default to port 8000. 48 | 4. ```npm run initialize-db``` to create all db tables with populated value (Local / cloud depending upon the config url) 49 | 5. At any moment you can use ```npm run reinitialize-db``` to flush all the data present in the tables. 50 | 6. For the current version auth should be used with cognito. 51 | 7. ```npm install -g serverless``` to install serverless globally 52 | 8. ```npm install``` in the backend repository. 53 | 9. ```npm run start``` will start the serverless backend offline. 54 | 10. Certain routes are protected by cognito pool. Change the `serverles.yaml` as per your cognito pool to have authenticated routes. 55 | 56 | **To setup Frontend:** 57 | 1. Create .env file under ```packages/CB-serverless-frontend``` folder with below variables: 58 | REACT_APP_REGION=XXXXXX 59 | REACT_APP_URL=http://localhost:3000 60 | REACT_APP_REGION=XXXXXX 61 | REACT_APP_USER_POOL_ID=XXXXXX 62 | REACT_APP_APP_CLIENT_ID=XXXXXX 63 | REACT_APP_IDENTITY_POOL_ID=XXXXXX 64 | 65 | you can use 4242 4242 4242 4242 for card number or refer stripe for more. 66 | 67 | 2. ```npm install``` to install 68 | 3. ```npm run start``` to start 69 | -------------------------------------------------------------------------------- /configs/formConfig.js: -------------------------------------------------------------------------------- 1 | const formConfig = { 2 | form: [ 3 | { 4 | type: 'text', 5 | props: { 6 | name: 'firstName', 7 | floatingLabelText: 'First Name' 8 | } 9 | }, 10 | { 11 | type: 'text', 12 | props: { 13 | name: 'lastName', 14 | floatingLabelText: 'Last Name' 15 | } 16 | }, 17 | { 18 | type: 'text', 19 | props: { 20 | name: 'password', 21 | type: 'password', 22 | floatingLabelText: 'Password' 23 | } 24 | }, 25 | { 26 | type: 'select', 27 | options: [ 28 | { label: 'USA', value: 'USA' }, 29 | { label: 'India', value: 'India' }, 30 | ], 31 | props: { 32 | name: 'country', 33 | floatingLabelText: 'Select Country' 34 | } 35 | }, 36 | { 37 | type: 'check', 38 | props: { 39 | name: 'subscribe', 40 | floatingLabelText: 'Subscribe ?' 41 | } 42 | }, 43 | { 44 | type: 'date', 45 | props: { 46 | name: 'dateOfBirth', 47 | floatingLabelText: 'DOB' 48 | } 49 | }, 50 | { 51 | type: 'toggle', 52 | props: { 53 | name: 'married', 54 | floatingLabelText: 'Are you married?' 55 | } 56 | }, 57 | ] 58 | } 59 | 60 | module.exports = { 61 | formConfig: formConfig, 62 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "0.0.0" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "lerna": "^2.11.0" 4 | }, 5 | "scripts": { 6 | "scaffold-form": "node scripts/scaffold-form.js" 7 | }, 8 | "dependencies": { 9 | "commander": "^2.15.1", 10 | "fs-extra": "^6.0.0", 11 | "lodash": "^4.17.10" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codebrahma/serverless-grocery-app/8e8d757934b73864ec2ab95a30cef6afee7d1d2d/packages/CB-serverless-backend/.DS_Store -------------------------------------------------------------------------------- /packages/CB-serverless-backend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["source-map-support", "transform-runtime"], 3 | "presets": [ 4 | ["env", { "node": "8.10" }], 5 | "stage-3" 6 | ] 7 | } 8 | 9 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anomaly Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codebrahma/serverless-grocery-app/8e8d757934b73864ec2ab95a30cef6afee7d1d2d/packages/CB-serverless-backend/README.md -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codebrahma/serverless-grocery-app/8e8d757934b73864ec2ab95a30cef6afee7d1d2d/packages/CB-serverless-backend/api/.DS_Store -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/cart/createCart.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | 3 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 4 | import getErrorResponse from '../../utils/getErrorResponse'; 5 | import getSuccessResponse from '../../utils/getSuccessResponse'; 6 | import { CART_TABLE_NAME } from '../../dynamoDb/constants'; 7 | 8 | awsConfigUpdate(); 9 | 10 | /* 11 | * Creates a cart with the list of groceries for a particular user 12 | * This cart will be used once an order is created during checkout 13 | * */ 14 | export const main = (event, context, callback) => { 15 | context.callbackWaitsForEmptyEventLoop = false; 16 | 17 | const documentClient = new AWS.DynamoDB.DocumentClient(); 18 | 19 | const { 20 | userId, 21 | cartData, 22 | } = JSON.parse(event.body); 23 | 24 | var params = { 25 | TableName: CART_TABLE_NAME, 26 | Key: { 27 | userId: userId, 28 | }, 29 | ExpressionAttributeNames: { 30 | '#cartData': 'cartData' 31 | }, 32 | ExpressionAttributeValues: { 33 | ':cartData': cartData, 34 | }, 35 | UpdateExpression: 'SET #cartData = :cartData', 36 | ReturnValues: 'ALL_NEW', 37 | }; 38 | 39 | const queryPromise = documentClient.update(params).promise(); 40 | 41 | queryPromise 42 | .then((data) => { 43 | callback(null, getSuccessResponse(data)) 44 | }) 45 | .catch((error) => { 46 | console.log(error); 47 | callback(null, getErrorResponse(500, JSON.stringify(error.message))) 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/cart/getCart.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | 3 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 4 | import getErrorResponse from '../../utils/getErrorResponse'; 5 | import getSuccessResponse from '../../utils/getSuccessResponse'; 6 | import { CART_TABLE_NAME } from '../../dynamoDb/constants'; 7 | 8 | import { getCartQueryPromise } from '../utils'; 9 | 10 | awsConfigUpdate(); 11 | const documentClient = new AWS.DynamoDB.DocumentClient(); 12 | 13 | /* 14 | * Gets the cart details of an user 15 | * */ 16 | export const main = (event, context, callback) => { 17 | context.callbackWaitsForEmptyEventLoop = false; 18 | if (!event.queryStringParameters) { 19 | callback(null, getErrorResponse(400, 'userId is not present in params')) 20 | return; 21 | } 22 | 23 | getCartQueryPromise(event.queryStringParameters.userId) 24 | .then((data) => { 25 | callback(null, getSuccessResponse(data)) 26 | }) 27 | .catch((error) => { 28 | console.log(error.message); 29 | callback(null, getErrorResponse(500, JSON.stringify(error.message))) 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/cart/getCartWithDetails.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import size from 'lodash/size'; 3 | import map from 'lodash/map'; 4 | import reduce from 'lodash/reduce'; 5 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 6 | import getErrorResponse from '../../utils/getErrorResponse'; 7 | import getSuccessResponse from '../../utils/getSuccessResponse'; 8 | import { getCartQueryPromise } from '../utils'; 9 | import { CART_TABLE_NAME, GROCERIES_TABLE_NAME } from '../../dynamoDb/constants'; 10 | 11 | awsConfigUpdate(); 12 | const documentClient = new AWS.DynamoDB.DocumentClient(); 13 | 14 | /* 15 | * Gets cart items with each grocery details 16 | * */ 17 | export const main = (event, context, callback) => { 18 | context.callbackWaitsForEmptyEventLoop = false; 19 | 20 | if (!event.queryStringParameters) { 21 | getErrorResponse(callback, 400, 'userId is not present in params'); 22 | return; 23 | } 24 | 25 | let groceryIdToGroceryDataMapping; 26 | getCartQueryPromise(event.queryStringParameters.userId) 27 | .then((cart) => { 28 | 29 | const cartItems = cart.Item ? cart.Item.cartData : []; 30 | 31 | if (!cart || size(cartItems) < 1) { 32 | callback(null, getSuccessResponse({success: false, message: 'Cart is empty'})); 33 | return; 34 | } 35 | groceryIdToGroceryDataMapping = reduce(cartItems, (currentReducedValue, productInCart) => { 36 | return { 37 | ...currentReducedValue, 38 | [productInCart.groceryId] : productInCart 39 | } 40 | }, {}); 41 | return getCartItemDetails(cartItems); 42 | }) 43 | .then(dbResult => dbResult.Responses.grocery) 44 | .then(cartItemsWithDetails => { 45 | const fullCartDetails = map(cartItemsWithDetails, eachCartItemData => ({ 46 | ...eachCartItemData, 47 | ...groceryIdToGroceryDataMapping[eachCartItemData.groceryId] 48 | })) 49 | callback(null, getSuccessResponse(fullCartDetails)) 50 | }) 51 | .catch((error) => { 52 | console.log(error.message); 53 | callback(null, getErrorResponse(500, JSON.stringify(error.message))) 54 | }); 55 | } 56 | 57 | const getCartItemDetails = (cartData) => { 58 | const keysForBatchGet = map(cartData, item => ({'groceryId': item.groceryId})) 59 | const paramsForBatchGet = { 60 | RequestItems: { 61 | [GROCERIES_TABLE_NAME] : { 62 | Keys: keysForBatchGet 63 | } 64 | } 65 | }; 66 | 67 | return documentClient.batchGet(paramsForBatchGet).promise(); 68 | } 69 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/groceries/getGroceries.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import _ from 'lodash'; 3 | import filter from 'lodash/filter'; 4 | import uniqBy from 'lodash/uniqBy'; 5 | import map from 'lodash/map'; 6 | 7 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 8 | import getErrorResponse from '../../utils/getErrorResponse'; 9 | import getSuccessResponse from '../../utils/getSuccessResponse'; 10 | import { GROCERIES_TABLE_NAME, GROCERIES_TABLE_GLOBAL_INDEX_NAME, PAGINATION_DEFAULT_OFFSET } from '../../dynamoDb/constants'; 11 | 12 | awsConfigUpdate(); 13 | 14 | /* 15 | * Gets all grocery items with pagination 16 | * Read more about dynamodb Pagination https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html 17 | * */ 18 | export const main = (event, context, callback) => { 19 | context.callbackWaitsForEmptyEventLoop = false; 20 | 21 | const documentClient = new AWS.DynamoDB.DocumentClient(); 22 | 23 | // Base params for scanning 24 | const getBaseGroceriesParams = () => ({ 25 | TableName: GROCERIES_TABLE_NAME, 26 | ExpressionAttributeNames: { 27 | '#groceryId': 'groceryId', 28 | '#category': 'category', 29 | '#subCategory': 'subCategory', 30 | '#name': 'name', 31 | '#url': 'url', 32 | '#availableQty': 'availableQty', 33 | '#soldQty': 'soldQty', 34 | '#price': 'price', 35 | }, 36 | ProjectionExpression: "#groceryId, #category, #subCategory, #name, #url, #availableQty, #soldQty, #price", 37 | }); 38 | 39 | // If category exists then return the listings for that category 40 | if (event.queryStringParameters && event.queryStringParameters.category) { 41 | const { category, limit } = event.queryStringParameters 42 | let params = { 43 | ...getBaseGroceriesParams(), 44 | Limit: limit || PAGINATION_DEFAULT_OFFSET, 45 | IndexName: GROCERIES_TABLE_GLOBAL_INDEX_NAME, 46 | KeyConditionExpression: `#category = :categoryToFilter`, 47 | ExpressionAttributeValues: { 48 | ':categoryToFilter': category 49 | }, 50 | }; 51 | 52 | // If nextPageIndex is present, fetch the list as required 53 | if (event.queryStringParameters.nextPageIndex) { 54 | params = { 55 | ...params, 56 | ExclusiveStartKey: { 57 | 'category': category, 58 | 'groceryId': event.queryStringParameters.nextPageIndex, 59 | } 60 | } 61 | } 62 | 63 | const queryPromise = documentClient.query(params).promise(); 64 | 65 | // Provide next Page index to frontend if more items are available 66 | queryPromise 67 | .then((data) => { 68 | const responseData = { 69 | Items: data.Items, 70 | nextPageParams: data.LastEvaluatedKey ? `nextPageIndex=${data.LastEvaluatedKey.groceryId}` : '', 71 | } 72 | callback(null, getSuccessResponse(responseData)) 73 | }) 74 | .catch((error) => { 75 | callback(null, getErrorResponse(500, 'Unable to fetch! Try again later')); 76 | }); 77 | } else { 78 | // If not scan and filter categories and bring the top 3 items, 79 | var params = getBaseGroceriesParams(); 80 | 81 | const queryPromise = documentClient.scan(params).promise(); 82 | 83 | // Does a pre processing to show response 84 | queryPromise 85 | .then((data) => { 86 | const uniqueCategories = _ 87 | .chain(data.Items) 88 | .uniqBy('category') 89 | .map(data => data.category) 90 | .map((category) => { 91 | const filteredResult = _ 92 | .chain(data.Items) 93 | .filter(grocery => (grocery.category === category)) 94 | .orderBy(['soldQty'], ['desc']) 95 | .take(3) 96 | .value(); 97 | 98 | return { 99 | category, 100 | groceries: filteredResult, 101 | } 102 | }) 103 | .value(); 104 | 105 | // Sends the response 106 | callback(null, getSuccessResponse(uniqueCategories)) 107 | }) 108 | .catch((error) => { 109 | callback(null, getErrorResponse(500, JSON.stringify(error.message))); 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/groceries/getGrocery.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import AWS from 'aws-sdk'; 3 | 4 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 5 | import getErrorResponse from '../../utils/getErrorResponse'; 6 | import getSuccessResponse from '../../utils/getSuccessResponse'; 7 | import { GROCERIES_TABLE_NAME } from '../../dynamoDb/constants'; 8 | 9 | awsConfigUpdate(); 10 | 11 | /* 12 | * Gets a grocery based on groceryId 13 | * */ 14 | export const main = (event, context, callback) => { 15 | context.callbackWaitsForEmptyEventLoop = false; 16 | 17 | var documentClient = new AWS.DynamoDB.DocumentClient(); 18 | 19 | if (!event.queryStringParameters || !event.queryStringParameters.id) { 20 | callback(null, getErrorResponse(400, 'id should be provided')); 21 | return; 22 | } 23 | 24 | var params = { 25 | TableName: GROCERIES_TABLE_NAME, 26 | Key: { 27 | groceryId: event.queryStringParameters.id, 28 | } 29 | }; 30 | 31 | const responsePromise = documentClient.get(params).promise(); 32 | 33 | responsePromise 34 | .then((data) => { 35 | callback(null, getSuccessResponse(data)); 36 | }) 37 | .catch((err) => { 38 | callback(null, getErrorResponse(500, JSON.stringify(err.message))); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/groceries/stock.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import forEach from 'lodash/forEach'; 3 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 4 | import getErrorResponse from '../../utils/getErrorResponse'; 5 | import getSuccessResponse from '../../utils/getSuccessResponse'; 6 | import { GROCERIES_TABLE_NAME } from '../../dynamoDb/constants'; 7 | 8 | awsConfigUpdate(); 9 | 10 | /* 11 | * Used to update stock when the order is made 12 | * */ 13 | export const updateStock = async (event, context, callback) => { 14 | context.callbackWaitsForEmptyEventLoop = false; 15 | const dataToUpdate = JSON.parse(event.body); 16 | const documentClient = new AWS.DynamoDB.DocumentClient(); 17 | const promiseArray = []; 18 | 19 | forEach(dataToUpdate, ({ groceryId, availableQty }) => { 20 | if (!groceryId || !availableQty) { 21 | callback(null, getErrorResponse(400, 'Missing or invalid data')); 22 | return; 23 | } 24 | 25 | const params = { 26 | TableName: GROCERIES_TABLE_NAME, 27 | Key: { 28 | 'groceryId': groceryId, 29 | }, 30 | UpdateExpression: `set availableQty=:updatedQty`, 31 | ExpressionAttributeValues: { 32 | ":updatedQty": availableQty, 33 | }, 34 | ReturnValues: "UPDATED_NEW" 35 | }; 36 | promiseArray.push(documentClient.update(params).promise()) 37 | }); 38 | 39 | Promise.all(promiseArray) 40 | .then(data => callback(null, getSuccessResponse({ success: true }))) 41 | .catch(error => callback(null, getErrorResponse(500, error))) 42 | }; 43 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/order/cancelOrder.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import reduce from 'lodash/reduce'; 3 | import map from 'lodash/map'; 4 | import size from 'lodash/size'; 5 | import findIndex from 'lodash/findIndex' 6 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 7 | import getErrorResponse from '../../utils/getErrorResponse'; 8 | import getSuccessResponse from '../../utils/getSuccessResponse'; 9 | import generateId from '../../utils/orderIdGenerator'; 10 | import { ORDERS_TABLE_NAME, GROCERIES_TABLE_NAME, CART_TABLE_NAME } from '../../dynamoDb/constants'; 11 | import { batchUpdateAvailableAndSoldQuantities } from '../utils'; 12 | 13 | awsConfigUpdate(); 14 | const documentClient = new AWS.DynamoDB.DocumentClient(); 15 | 16 | /* 17 | * Cancels an order 18 | * Returns the stock to the stock 19 | * Sets the status of order to "Cancelled" 20 | * */ 21 | export const main = (event, context, callback) => { 22 | const { 23 | userId, 24 | orderId 25 | } = JSON.parse(event.body); 26 | 27 | if (!userId || !orderId) { 28 | callback(null, getErrorResponse(400, 'Missing or invalid data')); 29 | return; 30 | } 31 | 32 | let orderToUpdate; 33 | getOrderDetails(userId, orderId) 34 | .then(dbResultSet => dbResultSet.Item) 35 | .then(orderData => { 36 | if (!orderData) { 37 | throw { 38 | message: 'Cannot find the order for given order id' 39 | }; 40 | } 41 | 42 | orderToUpdate = orderData; 43 | 44 | // Updates order status 45 | return UpdateOrderStatus(userId, orderId, 'CANCELLED') 46 | .catch(err => { 47 | return Promise.reject(err); 48 | }) 49 | .then(() => batchUpdateAvailableAndSoldQuantities(orderToUpdate.orderItems, true)) 50 | .catch(err => { 51 | return Promise.reject(err); 52 | }) 53 | }) 54 | .then(() => callback(null, getSuccessResponse({ success: true }))) 55 | .catch(err => { 56 | callback(null, getErrorResponse(400, `Error updating order. ${err.message}`)); 57 | return; 58 | }); 59 | } 60 | 61 | // changes order status 62 | export const UpdateOrderStatus = (userId, orderId, orderStatus) => { 63 | var updateParams = { 64 | TableName: ORDERS_TABLE_NAME, 65 | Key: { 66 | "userId": userId, 67 | "orderId": orderId, 68 | }, 69 | UpdateExpression: "set orderStatus=:newStatus", 70 | ExpressionAttributeValues: { 71 | ":newStatus": orderStatus, 72 | }, 73 | ReturnValues: "UPDATED_NEW" 74 | }; 75 | return documentClient.update(updateParams).promise(); 76 | } 77 | 78 | // Gets the order details first to update 79 | const getOrderDetails = (userId, orderId) => { 80 | const queryOrderParams = { 81 | TableName: ORDERS_TABLE_NAME, 82 | Key: { 83 | "userId": userId, 84 | "orderId": orderId, 85 | } 86 | } 87 | 88 | return documentClient.get(queryOrderParams).promise(); 89 | } -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/order/createOrder.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import reduce from 'lodash/reduce'; 3 | import map from 'lodash/map'; 4 | import size from 'lodash/size'; 5 | import findIndex from 'lodash/findIndex' 6 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 7 | import getErrorResponse from '../../utils/getErrorResponse'; 8 | import getSuccessResponse from '../../utils/getSuccessResponse'; 9 | import generateId from '../../utils/orderIdGenerator'; 10 | import { ORDERS_TABLE_NAME, GROCERIES_TABLE_NAME, CART_TABLE_NAME } from '../../dynamoDb/constants'; 11 | import { batchUpdateAvailableAndSoldQuantities } from '../utils'; 12 | 13 | awsConfigUpdate(); 14 | const documentClient = new AWS.DynamoDB.DocumentClient(); 15 | 16 | /* 17 | * Creates an order 18 | * Retrives the current cart items and creates an order 19 | * Updates the order and stocks 20 | * */ 21 | export const main = (event, context, callback) => { 22 | context.callbackWaitsForEmptyEventLoop = false; 23 | 24 | const { 25 | userId 26 | } = JSON.parse(event.body); 27 | if (!userId) { 28 | callback(null, getErrorResponse(400, 'Missing or invalid data')); 29 | return; 30 | } 31 | let idToGroceryDataMapping, cartItems, completeOrder; 32 | 33 | // Retrives the current Cart 34 | getCurrentCart(userId) 35 | .then(cart => { 36 | cartItems = cart.Item.cartData; 37 | 38 | if (!cartItems || size(cartItems) < 1) { 39 | callback(null, getSuccessResponse({ success: false, message: 'Cart is empty' })); 40 | return; 41 | } 42 | 43 | idToGroceryDataMapping = reduce(cartItems, (currentReducedValue, productInCart) => { 44 | return { 45 | ...currentReducedValue, 46 | [productInCart.groceryId]: productInCart 47 | } 48 | }, {}); 49 | // Modifies the structure for easy manipulation 50 | return getPricesOfCartItems(cartItems); 51 | }) 52 | .then(dbData => dbData.Responses.grocery) 53 | .then(cartItemsWithPrice => map(cartItemsWithPrice, cartItem => { 54 | return { 55 | ...cartItem, 56 | ...idToGroceryDataMapping[cartItem.groceryId] 57 | } 58 | }) 59 | ) 60 | .then(cartItemsWithPriceAndQty => reduce(cartItemsWithPriceAndQty, (currentTotal, currentItem) => { 61 | return currentTotal + (currentItem.qty * currentItem.price) 62 | }, 0) 63 | ) 64 | .then(totalAmount => { 65 | // Completes the order with the current state and pending payment 66 | completeOrder = { 67 | 'orderId': generateId(), 68 | 'userId': userId, 69 | 'orderItems': idToGroceryDataMapping, 70 | 'orderTotal': totalAmount, 71 | 'orderStatus': 'PAYMENT_PENDING', 72 | 'orderDate': new Date().toISOString() 73 | } 74 | // First update the stock 75 | // If success then place the order 76 | // If error then don't place the order 77 | return batchUpdateAvailableAndSoldQuantities(idToGroceryDataMapping) 78 | .catch((err) => { 79 | // Rejecting only with err since err.message has been extracted in the batch update function 80 | return Promise.reject(err); 81 | }) 82 | // Creates the order 83 | .then(() => createAndSaveOrder(completeOrder)) 84 | .catch((err) => { 85 | return Promise.reject(err.message); 86 | }) 87 | 88 | }) 89 | .then(() => { 90 | callback(null, getSuccessResponse({ 91 | success: true, 92 | orderId: completeOrder.orderId, 93 | orderTotal: completeOrder.orderTotal 94 | })); 95 | }) 96 | .catch(error => { 97 | console.log(error); 98 | callback(null, getErrorResponse(500, error)) 99 | }); 100 | } 101 | 102 | // Order creation Query Promise 103 | const createAndSaveOrder = (orderData) => { 104 | const createOrderParams = { 105 | TableName: ORDERS_TABLE_NAME, 106 | Item: { 107 | ...orderData 108 | } 109 | } 110 | 111 | return documentClient.put(createOrderParams).promise(); 112 | }; 113 | 114 | // Get Cart Item Price Query Promise 115 | const getPricesOfCartItems = (cartData) => { 116 | const keysForBatchGet = map(cartData, item => ({ 'groceryId': item.groceryId })) 117 | const paramsForBatchGet = { 118 | RequestItems: { 119 | [GROCERIES_TABLE_NAME]: { 120 | Keys: keysForBatchGet, 121 | ProjectionExpression: 'groceryId, price' 122 | } 123 | } 124 | }; 125 | 126 | return documentClient.batchGet(paramsForBatchGet).promise(); 127 | } 128 | 129 | // Get CurrentCart Query Promise 130 | const getCurrentCart = (userId) => { 131 | const params = { 132 | TableName: CART_TABLE_NAME, 133 | Key: { 134 | 'userId': userId, 135 | }, 136 | }; 137 | 138 | return documentClient.get(params).promise(); 139 | } 140 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/order/getOrders.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import _ from 'lodash'; 3 | import filter from 'lodash/filter'; 4 | import uniqBy from 'lodash/uniqBy'; 5 | import map from 'lodash/map'; 6 | import reduce from 'lodash/reduce'; 7 | 8 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 9 | import getErrorResponse from '../../utils/getErrorResponse'; 10 | import getSuccessResponse from '../../utils/getSuccessResponse'; 11 | import { ORDERS_TABLE_NAME, GROCERIES_TABLE_NAME } from '../../dynamoDb/constants'; 12 | 13 | awsConfigUpdate(); 14 | 15 | /* 16 | * Get orders based on ids 17 | * Each order will be having details about each of the item 18 | * */ 19 | export const main = (event, context, callback) => { 20 | context.callbackWaitsForEmptyEventLoop = false; 21 | if (!event.queryStringParameters || !event.queryStringParameters.userId) { 22 | callback(null, getErrorResponse(400, 'userId is not present in params')) 23 | return; 24 | } 25 | 26 | const documentClient = new AWS.DynamoDB.DocumentClient(); 27 | const userId = event.queryStringParameters.userId; 28 | const queryOrdersParams = { 29 | TableName: ORDERS_TABLE_NAME, 30 | KeyConditionExpression: "#userId = :userId", 31 | ExpressionAttributeNames: { 32 | "#userId": "userId", 33 | }, 34 | ExpressionAttributeValues: { 35 | ":userId": userId 36 | } 37 | } 38 | 39 | var getGroceryParams = (id) => ({ 40 | TableName: GROCERIES_TABLE_NAME, 41 | Key: { 42 | groceryId: id, 43 | } 44 | }); 45 | 46 | const groceryPromise = id => documentClient.get(getGroceryParams(id)).promise(); 47 | 48 | documentClient.query(queryOrdersParams).promise() 49 | .then(dbResultSet => { 50 | const result = map(dbResultSet.Items, async (item) => { 51 | const groceryListPromise = map(item.orderItems, (value, groceryId) => { 52 | return groceryPromise(groceryId); 53 | }); 54 | var itemList = await Promise.all(groceryListPromise); 55 | 56 | const updatedItemList = itemList.map(({ Item }) => { 57 | return Item; 58 | }); 59 | // Creates a current order list 60 | const currentOrderList = map(updatedItemList, (eachItem) => { 61 | return { 62 | ...eachItem, 63 | ...item.orderItems[eachItem.groceryId], 64 | }; 65 | }); 66 | 67 | const eachOrder = { 68 | ...item, 69 | orderItems: currentOrderList, 70 | } 71 | 72 | // Returns the list with promise 73 | return Promise.resolve(eachOrder); 74 | }); 75 | // Promise which returns all order data 76 | Promise 77 | .all(result) 78 | .then((data) => { 79 | callback(null, getSuccessResponse(data)); 80 | }) 81 | }) 82 | .catch(error => callback(null, getErrorResponse(500, JSON.stringify(error.message)))) 83 | } -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/pay/makePayment.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | 3 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 4 | import getErrorResponse from '../../utils/getErrorResponse'; 5 | import getSuccessResponse from '../../utils/getSuccessResponse'; 6 | import { ORDERS_TABLE_NAME } from '../../dynamoDb/constants'; 7 | import { UpdateOrderStatus } from '../order/cancelOrder'; 8 | 9 | // Put the stripe key in env 10 | const stripe = require("stripe")('sk_test_JEVvHOWUTi2mP5IA1rebWCdi'); 11 | 12 | awsConfigUpdate(); 13 | 14 | const documentClient = new AWS.DynamoDB.DocumentClient(); 15 | 16 | const getAmountFromOrderId = (orderId, userId) => { 17 | const params = { 18 | TableName: ORDERS_TABLE_NAME, 19 | Key: { 20 | 'userId': userId, 21 | 'orderId': orderId, 22 | }, 23 | 24 | ProjectionExpression: "orderId, orderTotal", 25 | } 26 | 27 | return documentClient.get(params).promise(); 28 | } 29 | 30 | /* 31 | * API which makes payment to stripe 32 | * */ 33 | export const main = (event, context, callback) => { 34 | context.callbackWaitsForEmptyEventLoop = false; 35 | 36 | const { 37 | email, 38 | stripeId, 39 | orderId, 40 | userId 41 | } = JSON.parse(event.body); 42 | 43 | if (!email || !stripeId || !orderId) { 44 | callback(null, getErrorResponse(400, JSON.stringify({ 45 | message: 'Both Email and id is required' 46 | }))) 47 | return; 48 | } 49 | 50 | let amount; 51 | 52 | getAmountFromOrderId(orderId, userId) 53 | .then((response) => { 54 | amount = response.Item.orderTotal; 55 | }) 56 | .catch((error) => { 57 | callback(null, getErrorResponse(500, JSON.stringify(error.message))) 58 | }) 59 | 60 | stripe.customers.create({ 61 | email, 62 | card: stripeId, 63 | }) 64 | .then(customer => 65 | stripe.charges.create({ 66 | amount, 67 | description: "Sample Charge", 68 | currency: "usd", 69 | customer: customer.id 70 | })) 71 | .then(() => { 72 | UpdateOrderStatus(userId, orderId, 'COMPLETED') 73 | }) 74 | .then(() => { 75 | callback(null, getSuccessResponse({ 76 | success: true, 77 | })) 78 | }) 79 | .catch((err) => { 80 | callback(null, getErrorResponse(500, JSON.stringify(err.message))) 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/api/utils/index.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import { CART_TABLE_NAME, GROCERIES_TABLE_NAME } from '../../dynamoDb/constants'; 3 | import awsConfigUpdate from '../../utils/awsConfigUpdate'; 4 | import map from 'lodash/map'; 5 | 6 | awsConfigUpdate(); 7 | const documentClient = new AWS.DynamoDB.DocumentClient(); 8 | 9 | /* 10 | * Cart cart Items 11 | * */ 12 | export const getCartQueryPromise = (userId) => { 13 | var params = { 14 | TableName: CART_TABLE_NAME, 15 | Key: { 16 | 'userId': userId, 17 | }, 18 | }; 19 | 20 | return documentClient.get(params).promise(); 21 | } 22 | 23 | // BatchGet the current sold and available quantities 24 | const getAvailableAndSoldQuantityForGroceries = (cartItems) => { 25 | const keys = map(cartItems, (item) => { 26 | return { 27 | 'groceryId': item.groceryId, 28 | } 29 | }); 30 | const params = { 31 | RequestItems: { 32 | [GROCERIES_TABLE_NAME]: { 33 | Keys: keys, 34 | ProjectionExpression: 'groceryId, availableQty, soldQty' 35 | } 36 | }, 37 | 38 | } 39 | return documentClient.batchGet(params).promise(); 40 | } 41 | 42 | // Reverts or cancel reverts of an item - current data 43 | // By default revert is false which is - It is a placed order (opposite to cancelling) 44 | const updateAvailableAndSoldQuantities = (currentData, orderedQty, revert = false) => { 45 | const factor = revert ? -1 : 1; 46 | // Update available and sold qty 47 | const updatedAvailableQty = currentData.availableQty - (factor * orderedQty); 48 | const updatedSoldQty = currentData.soldQty + (factor * orderedQty); 49 | if (updatedAvailableQty < 0) { 50 | return Promise.reject({ 51 | message: `Not sufficient stock available for item id: ${currentData.groceryId}`, 52 | }); 53 | // throw new Error(`Not sufficient stock available for item id: ${currentData.groceryId}`); 54 | } 55 | const params = { 56 | TableName: GROCERIES_TABLE_NAME, 57 | Key: { 58 | 'groceryId': currentData.groceryId, 59 | }, 60 | UpdateExpression: `set availableQty=:availableQty, soldQty=:soldQty`, 61 | ExpressionAttributeValues: { 62 | ":availableQty": updatedAvailableQty > 0 ? updatedAvailableQty : 0, 63 | ":soldQty": updatedSoldQty > 0 ? updatedSoldQty : 0, 64 | }, 65 | ReturnValues: "UPDATED_NEW" 66 | }; 67 | 68 | return documentClient.update(params).promise(); 69 | } 70 | 71 | 72 | export const batchUpdateAvailableAndSoldQuantities = (groceryIdToGroceryItemMap, revert = false) => { 73 | // Get Current Values of the cart for groceryId present in cartItems 74 | return getAvailableAndSoldQuantityForGroceries(groceryIdToGroceryItemMap) 75 | .then(data => Promise.all( 76 | map(data.Responses.grocery, (eachGrocery) => { 77 | // Based on id, merge groceryId, qty, soldQt, availableQty 78 | const getEntireCartDetail = { 79 | ...groceryIdToGroceryItemMap[eachGrocery.groceryId], 80 | ...eachGrocery, 81 | }; 82 | const orderedQty = getEntireCartDetail.qty; 83 | // Updates the quantity 84 | return updateAvailableAndSoldQuantities(eachGrocery, orderedQty, revert); 85 | }) 86 | )) 87 | .catch((err) => { 88 | console.log(err); 89 | return Promise.reject(JSON.stringify(err.message)) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/dynamoDb/awsConfigUpdate.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var { config } = require('../utils/config'); 3 | 4 | module.exports = { 5 | awsConfigUpdate: function () { 6 | AWS.config.update({ 7 | region: config.dbRegion, 8 | endpoint: config.dbLocalUrl, 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/dynamoDb/constants.js: -------------------------------------------------------------------------------- 1 | export const GROCERIES_TABLE_NAME = 'grocery'; 2 | export const GROCERIES_TABLE_GLOBAL_INDEX_NAME = 'GroceryCategoryIndex' 3 | export const CART_TABLE_NAME = 'cart'; 4 | export const ORDERS_TABLE_NAME = 'orders'; 5 | export const PAGINATION_DEFAULT_OFFSET = 30; -------------------------------------------------------------------------------- /packages/CB-serverless-backend/dynamoDb/createTable.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const indexOf = require('lodash/indexOf'); 3 | const chalk = require('chalk'); 4 | const { awsConfigUpdate } = require('./awsConfigUpdate'); 5 | 6 | awsConfigUpdate(); 7 | 8 | const dynamodb = new AWS.DynamoDB(); 9 | /* Delete Tables */ 10 | const getDeleteParams = tableName => ({ 11 | TableName: tableName, 12 | }); 13 | 14 | const createGroceryTable = () => { 15 | const groceryParams = { 16 | TableName: 'grocery', 17 | KeySchema: [ 18 | { AttributeName: 'groceryId', KeyType: 'HASH' }, 19 | ], 20 | AttributeDefinitions: [ 21 | { AttributeName: 'groceryId', AttributeType: 'S' }, 22 | { AttributeName: 'category', AttributeType: 'S' }, 23 | ], 24 | ProvisionedThroughput: { 25 | ReadCapacityUnits: 2, 26 | WriteCapacityUnits: 2, 27 | }, 28 | GlobalSecondaryIndexes: [ 29 | { 30 | IndexName: 'GroceryCategoryIndex', 31 | KeySchema: [ 32 | { AttributeName: 'category', KeyType: 'HASH' }, 33 | { AttributeName: 'groceryId', KeyType: 'RANGE' }, 34 | ], 35 | Projection: { 36 | ProjectionType: 'ALL', 37 | }, 38 | ProvisionedThroughput: { 39 | ReadCapacityUnits: 2, 40 | WriteCapacityUnits: 2, 41 | }, 42 | }, 43 | ], 44 | }; 45 | return dynamodb.createTable(groceryParams).promise(); 46 | }; 47 | 48 | const createOrderTable = () => { 49 | const orderParams = { 50 | TableName: 'orders', 51 | KeySchema: [ 52 | { AttributeName: 'userId', KeyType: 'HASH' }, 53 | { AttributeName: 'orderId', KeyType: 'RANGE' }, 54 | ], 55 | AttributeDefinitions: [ 56 | { AttributeName: 'orderId', AttributeType: 'S' }, 57 | { AttributeName: 'userId', AttributeType: 'S' }, 58 | ], 59 | ProvisionedThroughput: { 60 | ReadCapacityUnits: 2, 61 | WriteCapacityUnits: 2, 62 | }, 63 | }; 64 | 65 | return dynamodb.createTable(orderParams).promise(); 66 | }; 67 | 68 | const createCartTable = () => { 69 | const userParams = { 70 | TableName: 'cart', 71 | KeySchema: [ 72 | { AttributeName: 'userId', KeyType: 'HASH' }, 73 | ], 74 | AttributeDefinitions: [ 75 | { AttributeName: 'userId', AttributeType: 'S' }, 76 | ], 77 | ProvisionedThroughput: { 78 | ReadCapacityUnits: 2, 79 | WriteCapacityUnits: 2, 80 | }, 81 | }; 82 | 83 | return dynamodb.createTable(userParams).promise(); 84 | }; 85 | let tables; 86 | 87 | const listTables = dynamodb.listTables({}).promise(); 88 | 89 | listTables 90 | .then((data, err) => { 91 | let groceryTablePromise, 92 | userTablePromise, 93 | orderTablePromise; 94 | 95 | groceryTablePromise = (indexOf(data.TableNames, 'grocery') === -1) ? createGroceryTable() : Promise.resolve(); 96 | userTablePromise = (indexOf(data.TableNames, 'cart') === -1) ? createCartTable() : Promise.resolve(); 97 | orderTablePromise = (indexOf(data.TableNames, 'order') === -1) ? createOrderTable() : Promise.resolve(); 98 | 99 | return Promise.all([groceryTablePromise, userTablePromise, orderTablePromise]); 100 | }) 101 | .then(() => { 102 | console.log(chalk.green('Created Tables Successfully')); 103 | }) 104 | .catch((e) => { 105 | console.log(chalk.red('Could not create tables. Reason: ', e.message)); 106 | }); 107 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/dynamoDb/data/sampleCart.js: -------------------------------------------------------------------------------- 1 | const cart = [{userId: "123456", cartData: [{groceryId: "4", qty: 2}, {groceryId: "37", qty: 4}]}, 2 | {userId: "123457", cartData: []}] 3 | 4 | module.exports = { 5 | cart, 6 | } -------------------------------------------------------------------------------- /packages/CB-serverless-backend/dynamoDb/deleteTable.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const indexOf = require('lodash/indexOf'); 3 | const chalk = require('chalk'); 4 | 5 | const { awsConfigUpdate } = require('./awsConfigUpdate'); 6 | 7 | awsConfigUpdate(); 8 | 9 | const dynamodb = new AWS.DynamoDB(); 10 | /* Delete Tables */ 11 | const getDeleteParams = tableName => ({ 12 | TableName: tableName, 13 | }); 14 | 15 | const tableName = [ 16 | 'grocery', 17 | 'cart', 18 | 'orders', 19 | ]; 20 | 21 | const deleteAllTablePromise = tableName.map(table => dynamodb.deleteTable(getDeleteParams(table)).promise()); 22 | 23 | Promise 24 | .all(deleteAllTablePromise) 25 | .then(() => { 26 | console.log(chalk.green('Deleted all tables successfully')); 27 | }) 28 | .catch((e) => { 29 | console.log(chalk.red('Could not delete tables. Reason: ', e.message)); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/dynamoDb/populateTable.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var fs = require('fs'); 3 | var chalk = require('chalk'); 4 | var { groceryList } = require('./data/groceryList'); 5 | var { cart } = require('./data/sampleCart'); 6 | const { awsConfigUpdate } = require('./awsConfigUpdate'); 7 | 8 | awsConfigUpdate(); 9 | 10 | 11 | var docClient = new AWS.DynamoDB.DocumentClient(); 12 | 13 | const groceryPromises = []; 14 | const cartPromise = []; 15 | 16 | groceryList.forEach(function (item) { 17 | var params = { 18 | TableName: 'grocery', 19 | Item: { 20 | groceryId: item.groceryId, 21 | name: item.name, 22 | url: item.url, 23 | category: item.category, 24 | subCategory: item.subCategory, 25 | price: item.price, 26 | availableQty: item.availableQty, 27 | soldQty: item.soldQty 28 | }, 29 | }; 30 | 31 | groceryPromises.push(docClient.put(params).promise()) 32 | }); 33 | 34 | cart.forEach(function (item) { 35 | var params = { 36 | TableName: 'cart', 37 | Item: { 38 | userId: item.userId, 39 | cartData: item.cartData, 40 | }, 41 | }; 42 | 43 | cartPromise.push(docClient.put(params).promise()); 44 | }); 45 | Promise 46 | .all(groceryPromises) 47 | .then(() => { 48 | return Promise.all(cartPromise) 49 | }) 50 | .then((data) => { 51 | console.log(chalk.green('Populated Tables successfully')); 52 | }) 53 | .catch((e) => { 54 | console.log(chalk.red('Could not populate tables. Reason: ', e.message)) 55 | }) 56 | 57 | 58 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/env.example: -------------------------------------------------------------------------------- 1 | # HOW TO USE: 2 | # 3 | # 1 Add environment variables for the various stages here 4 | # 2 Rename this file to env.yml and uncomment it's usage 5 | # in the serverless.yml. 6 | # 3 Make sure to not commit this file. 7 | 8 | dev: 9 | APP_NAME: serverless-nodejs-starter 10 | 11 | prod: 12 | APP_NAME: serverless-nodejs 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-nodejs-starter", 3 | "version": "1.1.0", 4 | "description": "A Node.js starter for the Serverless Framework with async/await and unit test support", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "serverless offline start", 9 | "initialize-db": "node dynamoDb/createTable.js && node dynamoDb/populateTable.js", 10 | "reinitialize-db": "node dynamoDb/deleteTable.js && npm run initialize-db" 11 | }, 12 | "author": "", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/AnomalyInnovations/serverless-nodejs-starter.git" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.26.0", 20 | "babel-loader": "^7.1.4", 21 | "babel-plugin-source-map-support": "^1.0.0", 22 | "babel-plugin-transform-runtime": "^6.23.0", 23 | "babel-preset-env": "^1.6.1", 24 | "babel-preset-stage-3": "^6.24.1", 25 | "jest": "^21.2.1", 26 | "serverless-offline": "^3.20.0", 27 | "serverless-webpack": "^5.1.0", 28 | "webpack": "^4.2.0", 29 | "webpack-node-externals": "^1.6.0" 30 | }, 31 | "dependencies": { 32 | "aws-sdk": "^2.238.1", 33 | "babel-runtime": "^6.26.0", 34 | "bluebird": "^3.5.1", 35 | "chalk": "^2.4.1", 36 | "mongoose": "^5.0.16", 37 | "source-map-support": "^0.4.18", 38 | "stripe": "^6.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/serverless.yml: -------------------------------------------------------------------------------- 1 | # NOTE: update this with your service name 2 | service: grocery-app-api 3 | 4 | # Use the serverless-webpack plugin to transpile ES6 5 | plugins: 6 | - serverless-webpack 7 | - serverless-offline 8 | 9 | # serverless-webpack configuration 10 | # Enable auto-packing of external modules 11 | custom: 12 | webpack: 13 | webpackConfig: ./webpack.config.js 14 | includeModules: true 15 | 16 | provider: 17 | name: aws 18 | runtime: nodejs8.10 19 | stage: dev 20 | region: ap-south-1 21 | memorySize: 128 # set the maximum memory of the Lambdas in Megabytes 22 | timeout: 30 # the timeout is 10 seconds (default is 6 seconds) 23 | iamRoleStatements: 24 | - Effect: Allow 25 | Action: 26 | - cloudformation:DescribeStackResource 27 | - dynamodb:DescribeTable 28 | - dynamodb:Query 29 | - dynamodb:Scan 30 | - dynamodb:GetItem 31 | - dynamodb:PutItem 32 | - dynamodb:UpdateItem 33 | - dynamodb:DeleteItem 34 | Resource: "*" 35 | # To load environment variables externally 36 | # rename env.example to env.yml and uncomment 37 | # the following line. Also, make sure to not 38 | # commit your env.yml. 39 | # 40 | #environment: ${file(env.yml):${self:provider.stage}} 41 | 42 | functions: 43 | getGrocery: 44 | handler: api/groceries/getGrocery.main 45 | events: 46 | - http: 47 | path: grocery 48 | method: get 49 | cors: true 50 | getGroceries: 51 | handler: api/groceries/getGroceries.main 52 | events: 53 | - http: 54 | path: groceries 55 | method: get 56 | cors: true 57 | authorizer: 58 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 59 | updateStock: 60 | handler: api/groceries/stock.updateStock 61 | events: 62 | - http: 63 | path: updateStock 64 | method: post 65 | cors: true 66 | authorizer: 67 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 68 | createCart: 69 | handler: api/cart/createCart.main 70 | events: 71 | - http: 72 | path: cart 73 | method: post 74 | cors: true 75 | authorizer: 76 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 77 | getCart: 78 | handler: api/cart/getCart.main 79 | events: 80 | - http: 81 | path: cart 82 | method: get 83 | cors: true 84 | authorizer: 85 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 86 | getCartWithDetails: 87 | handler: api/cart/getCartWithDetails.main 88 | events: 89 | - http: 90 | path: cartDetails 91 | method: get 92 | cors: true 93 | authorizer: 94 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 95 | createOrder: 96 | handler: api/order/createOrder.main 97 | events: 98 | - http: 99 | path: createOrder 100 | method: post 101 | cors: true 102 | authorizer: 103 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 104 | getUserOrders: 105 | handler: api/order/getOrders.main 106 | events: 107 | - http: 108 | path: getOrders 109 | method: get 110 | cors: true 111 | authorizer: 112 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 113 | cancelOrder: 114 | handler: api/order/cancelOrder.main 115 | events: 116 | - http: 117 | path: cancelOrder 118 | method: post 119 | cors: true 120 | authorizer: 121 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG 122 | makePayment: 123 | handler: api/pay/makePayment.main 124 | events: 125 | - http: 126 | path: pay 127 | method: post 128 | cors: true 129 | authorizer: 130 | arn: arn:aws:cognito-idp:ap-south-1:872196253669:userpool/ap-south-1_50ZoISXiG -------------------------------------------------------------------------------- /packages/CB-serverless-backend/tests/handler.test.js: -------------------------------------------------------------------------------- 1 | // import * as handler from '../handler'; 2 | 3 | // test('hello', async () => { 4 | // const event = 'event'; 5 | // const context = 'context'; 6 | // const callback = (error, response) => { 7 | // expect(response.statusCode).toEqual(200); 8 | // expect(typeof response.body).toBe("string"); 9 | // }; 10 | 11 | // await handler.hello(event, context, callback); 12 | // }); 13 | -------------------------------------------------------------------------------- /packages/CB-serverless-backend/utils/awsConfigUpdate.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import { 3 | config 4 | } from './config'; 5 | 6 | export default () => { 7 | AWS.config.update({ 8 | region: config.dbRegion, 9 | endpoint: config.dbLocalUrl, 10 | }); 11 | }; -------------------------------------------------------------------------------- /packages/CB-serverless-backend/utils/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | config: { 3 | dbLocalUrl: 'http://localhost:8000', // Relace with https://dynamodb.ap-south-1.amazonaws.com before deployment 4 | dbRegion: 'ap-south-1', 5 | } 6 | }; -------------------------------------------------------------------------------- /packages/CB-serverless-backend/utils/getErrorResponse.js: -------------------------------------------------------------------------------- 1 | const getErrorResponse = (statusCode, errorMessage) => ({ 2 | statusCode: statusCode, 3 | headers: { 'Content-Type': 'application/json' }, 4 | body: { success: false, error: errorMessage }, 5 | }); 6 | 7 | export default getErrorResponse; -------------------------------------------------------------------------------- /packages/CB-serverless-backend/utils/getSuccessResponse.js: -------------------------------------------------------------------------------- 1 | const getResponse = (data) => ({ 2 | statusCode: 200, 3 | headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, 4 | body: JSON.stringify(data) 5 | }); 6 | 7 | export default getResponse; -------------------------------------------------------------------------------- /packages/CB-serverless-backend/utils/orderIdGenerator.js: -------------------------------------------------------------------------------- 1 | const idSuffix = () => Math.random().toString(36).substr(2, 9).toUpperCase(); 2 | const idPrefix = () => new Date().toISOString().substring(0, 10).replace(/-/g,""); 3 | const generateId = () => (`${idSuffix()}-${idPrefix()}`); 4 | 5 | export default generateId; -------------------------------------------------------------------------------- /packages/CB-serverless-backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require("serverless-webpack"); 2 | const nodeExternals = require("webpack-node-externals"); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: "node", 7 | // Generate sourcemaps for proper error messages 8 | devtool: 'source-map', 9 | // Since 'aws-sdk' is not compatible with webpack, 10 | // we exclude all node dependencies 11 | externals: [nodeExternals()], 12 | mode: slsw.lib.webpack.isLocal ? "development" : "production", 13 | optimization: { 14 | // We no not want to minimize our code. 15 | minimize: false 16 | }, 17 | performance: { 18 | // Turn off size warnings for entry points 19 | hints: false 20 | }, 21 | // Run babel on all .js files and skip those in node_modules 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | loader: "babel-loader", 27 | include: __dirname, 28 | exclude: /node_modules/ 29 | } 30 | ] 31 | } 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codebrahma/serverless-grocery-app/8e8d757934b73864ec2ab95a30cef6afee7d1d2d/packages/CB-serverless-frontend/.DS_Store -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "rules": { 8 | "linebreak-style": 0, 9 | "react/jsx-filename-extension": [ 10 | "error", { 11 | "extensions": [ 12 | ".js", 13 | ".jsx" 14 | ] 15 | } 16 | ] 17 | }, 18 | "parser": "babel-eslint", 19 | "extends": "airbnb", 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "experimentalObjectRestSpread": true, 23 | "jsx": true 24 | }, 25 | "sourceType": "module" 26 | }, 27 | "plugins": [ 28 | "react" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codebrahma/serverless-grocery-app/8e8d757934b73864ec2ab95a30cef6afee7d1d2d/packages/CB-serverless-frontend/README.md -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cb-serverless-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "aws-amplify": "^0.3.3", 7 | "axios": "^0.18.0", 8 | "eslint": "^4.19.1", 9 | "eslint-config-airbnb": "^16.1.0", 10 | "eslint-plugin-import": "^2.11.0", 11 | "eslint-plugin-jsx-a11y": "^6.0.3", 12 | "eslint-plugin-react": "^7.8.2", 13 | "js-beautify": "^1.7.5", 14 | "lodash": "^4.17.10", 15 | "material-ui": "^0.20.1", 16 | "moment": "^2.22.1", 17 | "prop-types": "^15.6.1", 18 | "react": "^16.3.2", 19 | "react-dom": "^16.3.2", 20 | "react-redux": "^5.0.7", 21 | "react-router-dom": "^4.2.2", 22 | "react-scripts": "1.1.4", 23 | "redux": "^4.0.0", 24 | "redux-devtools-extension": "^2.13.2", 25 | "redux-form": "^7.3.0", 26 | "redux-form-material-ui": "^4.3.4", 27 | "redux-logger": "^3.0.6", 28 | "redux-saga": "^0.16.0", 29 | "reselect": "^3.0.1", 30 | "styled-components": "^3.2.6" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test --env=jsdom", 36 | "eject": "react-scripts eject" 37 | }, 38 | "devDependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codebrahma/serverless-grocery-app/8e8d757934b73864ec2ab95a30cef6afee7d1d2d/packages/CB-serverless-frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 25 | React App 26 | 27 | 28 | 31 |
32 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/ForgotPasswordForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { TextField } from 'redux-form-material-ui'; 5 | import { RaisedButton } from 'material-ui'; 6 | import { ButtonSection, ButtonContainer } from './common/buttons'; 7 | 8 | /** 9 | ForgotPassword form, to reset the password. 10 | Submit the username, finally type code and new password. 11 | */ 12 | 13 | const validate = (values) => { 14 | const errors = {}; 15 | if (!values.username) { 16 | errors.username = "Email is required"; 17 | } 18 | if (!values.password) { 19 | errors.password = "Password is required"; 20 | } 21 | if (!values.code) { 22 | errors.code = "Code is required"; 23 | } 24 | return errors; 25 | } 26 | 27 | const renderNewPasswordInput = () => ( 28 | 29 |
30 | 35 |
36 |
37 | 43 |
44 |
45 | ); 46 | 47 | const renderUsernameInput = () => ( 48 |
49 | 54 |
55 | ); 56 | 57 | const renderButtons = ({inProgress, cancelAction}) => ( 58 | 59 | 60 | 66 | 67 | 68 | 73 | 74 | 75 | ); 76 | 77 | const ForgotPasswordForm = ({ handleSubmit, inProgress, cancelAction, passwordRequested }) => { 78 | return ( 79 |
80 |
81 | { 82 | passwordRequested ? 83 | renderNewPasswordInput() 84 | : 85 | renderUsernameInput() 86 | } 87 | {renderButtons({inProgress, cancelAction})} 88 |
89 |
90 | ) 91 | } 92 | 93 | ForgotPasswordForm.propTypes = { 94 | handleSubmit: PropTypes.func.isRequired, 95 | }; 96 | 97 | export default reduxForm({ 98 | form: 'forgotPassword', 99 | validate, 100 | destroyOnUnmount: false, 101 | })(ForgotPasswordForm); 102 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { TextField } from 'redux-form-material-ui'; 5 | import { RaisedButton } from 'material-ui'; 6 | import styled from 'styled-components'; 7 | 8 | const ForgotPassword = styled.div` 9 | color: #0db9f2; 10 | font-size: 12px; 11 | padding: 2%; 12 | cursor: pointer; 13 | `; 14 | 15 | /** 16 | Login form containing username and password fields 17 | containing option for forgot password 18 | */ 19 | 20 | const validate = (values) => { 21 | const errors = {}; 22 | 23 | if (!values.username) { 24 | errors.username = "Email is required"; 25 | } 26 | if (!values.password) { 27 | errors.password = "Password is required"; 28 | } 29 | if (!values.dueDate) { 30 | errors.dueDate = "Due Date is required"; 31 | } 32 | return errors; 33 | } 34 | 35 | const MyForm = ({ handleSubmit, inProgress, forgotPassword }) => { 36 | return ( 37 |
38 |
39 | 44 |
45 |
46 | 52 |
53 | forgotPassword(true)}> 54 | Forgot Password? 55 | 56 | 63 | 64 | ) 65 | } 66 | 67 | MyForm.propTypes = { 68 | handleSubmit: PropTypes.func.isRequired, 69 | }; 70 | 71 | export default reduxForm({ 72 | form: 'login', 73 | validate, 74 | destroyOnUnmount: false, 75 | })(MyForm); 76 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/RegisterForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { TextField } from 'redux-form-material-ui'; 5 | import { RaisedButton } from 'material-ui'; 6 | 7 | /** 8 | Registration form containing FullName, Email, Password, PhoneNumber fields. 9 | */ 10 | 11 | const validate = (values) => { 12 | const errors = {}; 13 | 14 | if (!values.name) { 15 | errors.name = "Name is required"; 16 | } 17 | if (!values.lastname) { 18 | errors.lastname = "Last Name is required"; 19 | } 20 | if (!values.username) { 21 | errors.username = "Email is required"; 22 | } 23 | if (!values.password) { 24 | errors.password = "Password is required"; 25 | } 26 | if (!values.phone) { 27 | errors.phone = "Phone number is required"; 28 | } 29 | 30 | return errors; 31 | } 32 | 33 | const MyForm = ({ handleSubmit, inProgress }) => { 34 | return ( 35 |
36 |
37 | 42 |
43 |
44 | 49 |
50 |
51 | 57 |
58 |
59 | 64 |
65 | 72 | 73 | ) 74 | } 75 | 76 | MyForm.propTypes = { 77 | handleSubmit: PropTypes.func.isRequired, 78 | }; 79 | 80 | export default reduxForm({ 81 | form: 'register', 82 | validate, 83 | destroyOnUnmount: false, 84 | })(MyForm); 85 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/VerificationForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { TextField } from 'redux-form-material-ui'; 5 | import { RaisedButton } from 'material-ui'; 6 | import { ButtonSection, ButtonContainer } from './common/buttons'; 7 | 8 | /** 9 | Verification form containing verifiacation-code field. 10 | */ 11 | 12 | const validate = (values) => { 13 | const errors = {}; 14 | if (!values.verification) { 15 | errors.verification = "Verification code is required"; 16 | } 17 | return errors; 18 | } 19 | 20 | const VerificationForm = ({ handleSubmit, cancelAction, inProgress }) => { 21 | return ( 22 |
23 |
24 |
25 | Enter the verification code. 26 |
27 |
28 | 33 |
34 | 35 | 36 | 42 | 43 | 44 | 49 | 50 | 51 |
52 |
53 | ) 54 | } 55 | 56 | VerificationForm.propTypes = { 57 | handleSubmit: PropTypes.func.isRequired, 58 | }; 59 | 60 | export default reduxForm({ 61 | form: 'verification', 62 | validate, 63 | destroyOnUnmount: false, 64 | })(VerificationForm); 65 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/actionCreators.js: -------------------------------------------------------------------------------- 1 | export const attemptLogin = (authScreen, requireVerification) => { 2 | return (requireVerification? { 3 | type: 'CONFIRM_VERIFICATION_CODE', 4 | payload: {authScreen} 5 | } : { 6 | type: 'ATTEMPT_LOGIN', 7 | payload: {authScreen} 8 | }); 9 | } 10 | 11 | export const requestCodeVerification = (authScreen) => ({ 12 | type: 'REQUEST_VERIFICATION_CODE', 13 | payload: authScreen 14 | }) 15 | 16 | 17 | export const updateAuth = (data) => ({ 18 | type: 'UPDATE_AUTH', 19 | payload: { 20 | ...data, 21 | }, 22 | }); 23 | 24 | export const forgotPasswordRequest = (requested) => { 25 | return (requested? {type: 'FORGOT_PASSWORD'} : {type: 'FORGOT_PASSWORD_REQUEST'}); 26 | }; 27 | 28 | export const clearCodeVerification = () => ({ 29 | type: 'CLEAR_CODE_VERIFICATION' 30 | }); 31 | 32 | export const clearForgotPasswordRequest = () => ({ 33 | type: 'CLEAR_FORGOT_PASSWORD' 34 | }) 35 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/authReducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | isAuthenticating: false, 3 | isAuthenticated: false, 4 | userData: null, 5 | identityId: null, 6 | inProgress: false, 7 | authError: { 8 | type: null, 9 | message: null 10 | }, 11 | passwordRequested: false, 12 | verifyUser: false 13 | } 14 | 15 | /** 16 | Store the authenticated userDetails along with accessToken. 17 | */ 18 | 19 | export default (state = initialState, { type, payload = {}}) => { 20 | switch(type) { 21 | case 'ATTEMPT_LOGIN_SUCCESS': 22 | return { 23 | ...state, 24 | isAuthenticating: false, 25 | isAuthenticated: true, 26 | authError: { 27 | type: null, 28 | message: null, 29 | }, 30 | identityId: payload.identityId, 31 | userData: payload.userData, 32 | inProgress: false, 33 | passwordRequested: false, 34 | verifyUser: false 35 | } 36 | case 'CLEAN_AUTH': 37 | return { 38 | isAuthenticating: false, 39 | isAuthenticated: false, 40 | authError: { 41 | type: null, 42 | message: null, 43 | }, 44 | identityId: null, 45 | userData: null, 46 | inProgress: false, 47 | passwordRequested: false, 48 | verifyUser: false 49 | } 50 | case 'ATTEMPT_LOGIN_FAILURE': 51 | return { 52 | ...state, 53 | isAuthenticating: false, 54 | isAuthenticated: false, 55 | isError: true, 56 | identityId: payload.identityId, 57 | userData: null, 58 | authError: { 59 | type: payload.type, 60 | message: payload.errorMessage, 61 | }, 62 | inProgress: false, 63 | } 64 | case 'VERIFICATION_CODE': 65 | return { 66 | ...state, 67 | inProgress: false, 68 | passwordRequested: false, 69 | verifyUser: true, 70 | } 71 | case 'UPDATE_AUTH': 72 | return { 73 | ...state, 74 | ...payload, 75 | inProgress: false, 76 | passwordRequested: false, 77 | verifyUser: false 78 | } 79 | case 'IN_PROGRESS': 80 | return { 81 | ...state, 82 | inProgress: true 83 | } 84 | case 'FORGOT_PASSWORD_REQUESTED': 85 | return { 86 | ...state, 87 | passwordRequested: true 88 | } 89 | case 'CLEAR_FORGOT_PASSWORD': 90 | return { 91 | ...state, 92 | passwordRequested: false 93 | } 94 | case 'CLEAR_CODE_VERIFICATION': 95 | return { 96 | ...state, 97 | verifyUser: false, 98 | } 99 | default: 100 | return state; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/common/buttons.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ButtonSection = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: center; 7 | margin-bottom: 3%; 8 | `; 9 | 10 | export const ButtonContainer = styled.div` 11 | padding: 2%; 12 | `; 13 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Tabs, Tab} from 'material-ui/Tabs'; 3 | import styled from 'styled-components'; 4 | 5 | import LoginForm from './LoginForm'; 6 | import RegisterForm from './RegisterForm'; 7 | import { connect } from 'react-redux'; 8 | import { bindActionCreators } from 'redux'; 9 | import {USER_NOT_VERIFIED, USER_ALREADY_EXIST} from '../constants/app'; 10 | import { RaisedButton } from 'material-ui'; 11 | import VerificationForm from './VerificationForm'; 12 | import ForgotPassword from './ForgotPasswordForm'; 13 | 14 | import { 15 | attemptLogin, 16 | requestCodeVerification, 17 | forgotPasswordRequest, 18 | clearCodeVerification, 19 | clearForgotPasswordRequest 20 | } from './actionCreators'; 21 | 22 | import styles from './styles.css'; 23 | 24 | const ButtonContainer = styled.div` 25 | padding: 5%; 26 | `; 27 | 28 | const LoginContainer = styled.div` 29 | background: #fff; 30 | margin: 2em auto; 31 | box-shadow: 0px 0px 25px 4px #ddd; 32 | borderRadius: '5%'; 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: flex-start; 36 | border-radius: 5%; 37 | overflow-x: auto; 38 | 39 | @media (min-width: 1280px) { 40 | width: 30%; 41 | } 42 | 43 | @media (max-width: 1279px){ 44 | width: 60%; 45 | } 46 | 47 | @media (min-width: 601px) and (max-width: 800px){ 48 | width: 100%; 49 | } 50 | 51 | @media (max-width: 600px){ 52 | width: 80%; 53 | transform: translateY(0); 54 | } 55 | 56 | @media (max-width: 480px){ 57 | width: 100%; 58 | margin: 0; 59 | transform: translateY(0); 60 | } 61 | 62 | `; 63 | 64 | /** 65 | Login/Registration authentication form 66 | */ 67 | 68 | class Login extends React.Component { 69 | constructor(props) { 70 | super(props); 71 | this.state = { 72 | authScreen: 'login', 73 | forgotPassword: false, 74 | verification: false, 75 | } 76 | this.props.history.push('/'); 77 | } 78 | 79 | handleChange = (authScreen) => { 80 | this.setState({ 81 | authScreen, 82 | }); 83 | } 84 | 85 | setForgotPassword = (value) => { 86 | this.props.clearForgotPasswordRequest(); 87 | this.setState({ 88 | forgotPassword: value 89 | }); 90 | } 91 | 92 | setVerification = (value) => { 93 | this.setState({ 94 | verification: value 95 | }); 96 | } 97 | 98 | displayErrorMessage = ({message}) => { 99 | const {authScreen} = this.state; 100 | return ( 101 |
102 | {message} 103 | { 104 | ( message === USER_NOT_VERIFIED || message === USER_ALREADY_EXIST ) && 105 | 106 | { 114 | message === USER_NOT_VERIFIED ? 115 | this.props.requestCodeVerification(authScreen) : 116 | this.setState({authScreen: 'login'}); 117 | } 118 | } 119 | /> 120 | 121 | } 122 |
123 | ); 124 | } 125 | 126 | renderForgotPassword = ({inProgress}) => ( 127 | (this.props.forgotPasswordRequest(this.props.passwordRequested))} 129 | cancelAction={() => this.setForgotPassword(false)} 130 | inProgress={inProgress} 131 | passwordRequested={this.props.passwordRequested} 132 | /> 133 | ); 134 | 135 | renderVerificationForm = () => ( 136 | { this.props.attemptLogin(this.state.authScreen, true) }} 138 | cancelAction={this.props.clearCodeVerification} 139 | /> 140 | ); 141 | 142 | renderLoginForm = ({inProgress}) => ( 143 | { this.props.attemptLogin(this.state.authScreen, false) }} 147 | /> 148 | ) 149 | 150 | renderRegistrationForm = ({inProgress}) => ( 151 | { 154 | this.props.attemptLogin(this.state.authScreen, false) 155 | }} 156 | /> 157 | ) 158 | 159 | renderForm = ({inProgress}) => { 160 | const {forgotPassword, authScreen} = this.state; 161 | return ( 162 | forgotPassword? 163 | this.renderForgotPassword({inProgress}) : 164 | ( 165 | this.props.verifyUser ? 166 | this.renderVerificationForm() : 167 | (authScreen === 'login' ? 168 | this.renderLoginForm({inProgress}) : 169 | this.renderRegistrationForm({inProgress})) 170 | ) 171 | ); 172 | } 173 | 174 | render() { 175 | const {authScreen} = this.state; 176 | const {message, type} = this.props.authError; 177 | const inProgress = (message === USER_NOT_VERIFIED || 178 | message === USER_ALREADY_EXIST)? false : this.props.inProgress; 179 | return ( 180 | 181 | 182 | 183 | 184 | 185 | 186 | { 187 | type === authScreen && !(this.props.verifyUser && message === USER_NOT_VERIFIED) && 188 | this.displayErrorMessage({message}) 189 | } 190 | { this.renderForm({inProgress})} 191 | 192 | 193 | ); 194 | } 195 | } 196 | 197 | 198 | const mapStateToProps = (state) => ({ 199 | authError: state.auth.authError, 200 | inProgress: state.auth.inProgress, 201 | passwordRequested: state.auth.passwordRequested, 202 | verifyUser: state.auth.verifyUser 203 | }); 204 | 205 | const mapDispatchToProps = (dispatch) => ({ 206 | attemptLogin: bindActionCreators(attemptLogin, dispatch), 207 | requestCodeVerification: bindActionCreators(requestCodeVerification, dispatch), 208 | forgotPasswordRequest: bindActionCreators(forgotPasswordRequest, dispatch), 209 | clearCodeVerification: bindActionCreators(clearCodeVerification, dispatch), 210 | clearForgotPasswordRequest: bindActionCreators(clearForgotPasswordRequest, dispatch), 211 | }); 212 | 213 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 214 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/Auth/styles.css: -------------------------------------------------------------------------------- 1 | .tab-container { 2 | width: 50%; 3 | margin: auto; 4 | padding: 15px; 5 | } 6 | 7 | .root-container { 8 | text-align: center; 9 | } 10 | 11 | .login-input { 12 | margin: '5% 0 7%'; 13 | } 14 | 15 | .success-note { 16 | border: 1px solid green; 17 | color: green; 18 | font-size: 12px; 19 | margin: 5% auto 0; 20 | padding: 2%; 21 | width: 80%; 22 | } 23 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/actions/cart.js: -------------------------------------------------------------------------------- 1 | export const fetchCartItems = () => ({ 2 | type: 'FETCH_CART_ITEMS', 3 | }); 4 | 5 | export const updateCartItems = (id, qty) => ({ 6 | type: 'UPDATE_CART_ITEMS', 7 | payload: { 8 | groceryId: id, 9 | qty, 10 | }, 11 | }); 12 | 13 | export const updateCartItemQty = (id, qty) => ({ 14 | type: 'UPDATE_CART_ITEM_QTY', 15 | payload: { 16 | groceryId: id, 17 | qty, 18 | }, 19 | }); 20 | 21 | export const deleteCartItem = id => ({ 22 | type: 'DELETE_CART_ITEM', 23 | payload: id, 24 | }); 25 | 26 | export const saveNewCart = data => ({ 27 | type: 'SAVE_NEW_CART', 28 | payload: data, 29 | }); 30 | 31 | 32 | export const saveItemInfoToCart = data => ({ 33 | type: 'SAVE_ITEM_INFO', 34 | payload: data, 35 | }); 36 | 37 | export const cleanCart = () => ({ 38 | type: 'CLEAN_CART_ITEMS', 39 | }); 40 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/actions/order.js: -------------------------------------------------------------------------------- 1 | export const fetchAllOrders = () => ({ 2 | type: 'FETCH_ALL_ORDERS', 3 | }); 4 | 5 | export const placeOrderAction = () => ({ 6 | type: 'PLACE_ORDER', 7 | }); 8 | 9 | export const cleanOrder = () => ({ 10 | type: 'CLEAN_ORDER', 11 | }); 12 | 13 | export const cancelOrder = () => ({ 14 | type: 'CANCEL_ORDER', 15 | }); 16 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/actions/payment.js: -------------------------------------------------------------------------------- 1 | export const submitPaymentTokenId = ({ 2 | tokenId, orderId, email, userId, 3 | }) => ({ 4 | type: 'SUBMIT_PAYMENT_TOKEN_ID', 5 | payload: { 6 | tokenId, orderId, email, userId, 7 | }, 8 | }); 9 | 10 | export const clearPayment = () => ({ 11 | type: 'CLEAR_PAYMENT', 12 | }); 13 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/CartItemSkeleton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | 4 | /** 5 | * Cart Item loading component 6 | */ 7 | const CartItemSkeleton = styled.div` 8 | margin: 1em 0.5em; 9 | width: 100%; 10 | background: #fff; 11 | background-repeat: no-repeat; 12 | background-image: 13 | linear-gradient(90deg, rgba(243,243,243,0) 0, rgba(243,243,243,0.4) 50%, 14 | rgba(243,243,243,0) 100%), 15 | linear-gradient(#f3f3f3 80px,transparent 0), // image 16 | linear-gradient(#f3f3f3 25px, transparent 0), // title 17 | linear-gradient(#f3f3f3 50px, transparent 0), // counter 18 | linear-gradient(#f3f3f3 50px, transparent 0); // price 19 | //linear-gradient(#fff 394px, transparent 0); 20 | background-size: 21 | 800px 100%, 22 | 80px 80px, 23 | 50% 25px, 24 | 140px 60px, 25 | 50px 50px 26 | //100% 100% 27 | ; 28 | background-position: 29 | -150% 0, 30 | 64px 32px, 31 | 30% 60px, 32 | 76% 50px, 33 | 85% 50px 34 | //0% 0%; 35 | ; 36 | height: 145px; 37 | animation: loadingCart 2s infinite; 38 | `; 39 | 40 | export default CartItemSkeleton; 41 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const FooterWrapper = styled.div` 5 | height: 300px; 6 | background: #333; 7 | color: #fff; 8 | padding: 2em 3em; 9 | `; 10 | 11 | const Footer = () => ( 12 | 13 | © CB 14 | 15 | ); 16 | 17 | export default Footer; 18 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/OrderButton.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import { RaisedButton } from 'material-ui'; 5 | import PropTypes from 'prop-types'; 6 | 7 | const CheckoutButton = styled(RaisedButton)` 8 | margin-bottom: 2em; 9 | > button { 10 | color: #fff; 11 | } 12 | `; 13 | 14 | /** 15 | * Order Button Component 16 | * @param backgroundColor {string} button bg color 17 | * @param fullWidth {boolean} 18 | * @param disabled {bool} 19 | * @param buttonStyle {object} 20 | * @param overlayStyle {object} style object of button overlay 21 | * @param onClick {func} callback 22 | * @param title {string} button text 23 | * @returns {*} 24 | * @constructor 25 | */ 26 | const OrderButton = ({ 27 | backgroundColor, fullWidth, disabled, buttonStyle, overlayStyle, onClick, title, 28 | }) => ( 29 | 50 | {title} 51 | 52 | ); 53 | 54 | OrderButton.defaultProps = { 55 | backgroundColor: '#6ca749', 56 | fullWidth: false, 57 | disabled: false, 58 | buttonStyle: {}, 59 | overlayStyle: {}, 60 | }; 61 | 62 | OrderButton.propTypes = { 63 | backgroundColor: PropTypes.string, 64 | fullWidth: PropTypes.bool, 65 | disabled: PropTypes.bool, 66 | buttonStyle: PropTypes.object, 67 | overlayStyle: PropTypes.object, 68 | onClick: PropTypes.func.isRequired, 69 | title: PropTypes.string.isRequired, 70 | }; 71 | 72 | 73 | export default OrderButton; 74 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/ProductImage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { CardMedia } from 'material-ui'; 3 | 4 | const ProductImageWrap = styled(CardMedia)` 5 | border-bottom: 1px solid #eee; 6 | padding: 10px; 7 | width: ${props => (props.width ? `${props.width}px` : '250px')}; 8 | height: ${props => (props.height ? `${props.height}px` : '250px')}; 9 | margin: 0 auto; 10 | opacity: ${props => (props.issoldout === 'true' ? '0.4' : '1')}; 11 | `; 12 | 13 | export default ProductImageWrap; 14 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/ProductSkeleton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | 4 | /** 5 | * Product Item loading component 6 | */ 7 | const ProductSkeleton = styled.div` 8 | margin: 1em 0.5em; 9 | width: 270px; 10 | background: #fff; 11 | background-repeat: no-repeat; 12 | background-image: 13 | linear-gradient(90deg, rgba(243,243,243,0) 0, rgba(243,243,243,0.4) 50%, 14 | rgba(243,243,243,0) 100%), 15 | linear-gradient(#f3f3f3 230px,transparent 0), // image 16 | linear-gradient(#f3f3f3 25px, transparent 0), // title 17 | linear-gradient(#f3f3f3 20px, transparent 0), // price 18 | linear-gradient(#f3f3f3 37px, transparent 0), // price 19 | linear-gradient(#fff 394px, transparent 0); 20 | background-size: 21 | 200px 100%, 22 | 90% 230px, 23 | 90% 25px, 24 | 80px 20px, 25 | 100px 37px, 26 | 100% 100%; 27 | background-position: 28 | -150% 0, 29 | 50% 20px, 30 | 50% 270px, 31 | 1em 310px, 32 | 90% 345px, 33 | 0% 0%; 34 | height: 394px; 35 | animation: loading 1s infinite; 36 | `; 37 | 38 | export default ProductSkeleton; 39 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/Quantity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { pink600 } from 'material-ui/styles/colors'; 5 | 6 | const CountSpan = styled.button` 7 | text-align: center; 8 | font-weight: bold; 9 | font-size: ${props => Math.max(props.size / 2, 15)}px; 10 | border-radius: ${props => (props.right ? '0px 4px 4px 0px' : '4px 0 0 4px')}; 11 | color: #f5f5f5; 12 | background: ${pink600} 13 | 14 | border-style: solid; 15 | border-color: #ddd; 16 | border-width: ${props => (props.right ? '1px 1px 1px 0px' : '1px 0 1px 1px')}; 17 | 18 | width: ${props => props.size}px; 19 | height: ${props => props.size}px; 20 | `; 21 | 22 | const CountInput = styled.input` 23 | color: ${pink600} 24 | height: ${props => props.size}px; 25 | width: ${props => props.size + 10}px; 26 | border-color: #ddd; 27 | border-width: 1px 0 1px; 28 | border-style: solid; 29 | padding-left: ${props => ((props.size + 10) / 2) - 4}px; 30 | `; 31 | 32 | const RowFlex = styled.div` 33 | display: flex; 34 | margin-right: 8px; 35 | flex-direction: row; 36 | justify-content: center; 37 | align-items: center; 38 | `; 39 | 40 | 41 | /** 42 | * Quantity Component 43 | * Contains increment and decrement buttons 44 | */ 45 | class Quantity extends React.PureComponent { 46 | constructor(props) { 47 | super(props); 48 | this.state = { 49 | count: this.props.initialQuantity, 50 | }; 51 | } 52 | 53 | dec = () => { 54 | const { disabled } = this.props; 55 | 56 | const { count: currentCount } = this.state; 57 | if (!disabled && currentCount >= 2) { // minimum quantity to be one 58 | this.setState((s, p) => ({ 59 | count: s.count - 1, 60 | }), () => this.props.onChange(this.state.count)); 61 | } 62 | }; 63 | 64 | inc = () => { 65 | const { disabled } = this.props; 66 | 67 | const { count: currentCount } = this.state; 68 | if (!disabled && currentCount < 10) { // minimum quantity to be one 69 | this.setState((s, p) => ({ 70 | count: s.count + 1, 71 | }), () => this.props.onChange(this.state.count)); 72 | } 73 | }; 74 | 75 | render() { 76 | const { size, disabled } = this.props; 77 | return ( 78 | 79 | - 83 | 84 | 94 | + 99 | 100 | 101 | ); 102 | } 103 | } 104 | 105 | Quantity.defaultProps = { 106 | initialQuantity: 1, 107 | disabled: false, 108 | size: 25, 109 | }; 110 | 111 | Quantity.propTypes = { 112 | onChange: PropTypes.func.isRequired, 113 | size: PropTypes.number, 114 | initialQuantity: PropTypes.number, 115 | disabled: PropTypes.bool, 116 | }; 117 | 118 | 119 | export default Quantity; 120 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/base_components/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.section` 4 | background: #fff; 5 | min-width: 300px; 6 | padding: 1em; 7 | 8 | @media (min-width: 1024px) { 9 | margin: 0.5em; 10 | padding: 1em 3em; 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Cart/CartItem.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types,react/no-did-mount-set-state */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { IconButton } from 'material-ui'; 5 | import Quantity from '../../base_components/Quantity'; 6 | import CartItemSkeleton from '../../base_components/CartItemSkeleton'; 7 | import { CartItemWrap, DeleteIconWrap, ItemImage, ItemTitle, SoldOutError } from './styles/components'; 8 | 9 | /** 10 | Item view with image, name and quantity to display in cart page, 11 | having option to delete the item or increase/decrease the quantity of it. 12 | */ 13 | 14 | class CartItem extends React.Component { 15 | displayName = 'CartItem Component'; 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | data: null, 21 | hasError: false, 22 | errorInfo: null, 23 | }; 24 | } 25 | 26 | 27 | componentDidMount() { 28 | const { info } = this.props; 29 | if (!this.state.data && info) { 30 | this.setState((s, p) => ({ 31 | data: info, 32 | })); 33 | } 34 | } 35 | 36 | /** 37 | * Lifecycle event to handle any error while displaying the cart item 38 | * @param error 39 | * @param info 40 | */ 41 | componentDidCatch(error, info) { 42 | this.setState((s, p) => ({ 43 | hasError: true, 44 | errorInfo: info, 45 | })); 46 | console.log(this.displayName, error, info); 47 | } 48 | 49 | /** 50 | * show the sold out message on cart Item if the quantity in cart is less 51 | * than server sent available Quantity 52 | * Not a validation, a UI warning message 53 | */ 54 | showSoldOutMessage = (availableQty) => { 55 | if (availableQty < this.props.qty) { 56 | return ( 57 | 58 | Item is Sold Out 59 | ); 60 | } 61 | return null; 62 | }; 63 | 64 | /** 65 | * Render cartItem delete from cart button 66 | */ 67 | renderDeleteIcon = () => ( 68 | 69 | this.props.onDelete(this.props.id)} 75 | iconClassName="material-icons" 76 | >delete 77 | 78 | ); 79 | 80 | 81 | render() { 82 | const { data } = this.state; 83 | 84 | /** 85 | * handle any error in component and render a error message 86 | */ 87 | if (this.state.hasError) { 88 | return ( 89 | 90 | Some error occured, cart item cant be displayed. ({this.state.errorInfo}) 91 | 92 | ); 93 | } 94 | 95 | 96 | /** 97 | * if data object from state, is empty or have a non-empty name return a skeleton loading effect 98 | */ 99 | if (!data || !data.name) { 100 | return (); 101 | } 102 | 103 | /** 104 | * Everything looks fine render the real component and show all info 105 | */ 106 | return ( 107 | 108 | 109 | 113 | 114 | 115 | {data.name} 116 | { 117 | this.showSoldOutMessage() 118 | } 119 | 120 | 121 | 122 | this.props.onQtyChange(this.props.id, qty)} 125 | initialQuantity={this.props.qty} 126 | /> 127 | 128 | { 129 | this.renderDeleteIcon() 130 | } 131 | 132 | 133 | ); 134 | } 135 | } 136 | 137 | CartItem.defaultProps = { 138 | info: null, 139 | }; 140 | 141 | CartItem.propTypes = { 142 | qty: PropTypes.number.isRequired, 143 | id: PropTypes.string.isRequired, 144 | info: PropTypes.object, 145 | onDelete: PropTypes.func.isRequired, 146 | onQtyChange: PropTypes.func.isRequired, 147 | }; 148 | 149 | export default CartItem; 150 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Cart/styles/components.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Wrapper } from '../../../base_components'; 3 | 4 | /** 5 | * Cart Home styled components 6 | */ 7 | 8 | export const CartWrapper = Wrapper.extend` 9 | color: #222; 10 | background: #f5f5f5; 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: space-between; 14 | align-items: stretch; 15 | `; 16 | 17 | export const CartMain = styled.div` 18 | flex: 7; 19 | padding: 3em 2em; 20 | background: #fff; 21 | box-shadow: 0 0 10px 1px #eee; 22 | `; 23 | 24 | export const EmptyCart = styled.div` 25 | padding: 2em; 26 | font-size: 20px; 27 | text-align: center; 28 | color: #888; 29 | background: #eee; 30 | margin: 4em auto 1em; 31 | `; 32 | 33 | export const CartHead = styled.h1` 34 | border-bottom: 1px solid #eee; 35 | padding-bottom: 1em; 36 | `; 37 | 38 | export const RightSideContent = styled.div` 39 | margin: 0 1em; 40 | flex: 2.5; 41 | color: #333; 42 | `; 43 | 44 | export const OrderPending = styled.section` 45 | background: #fff; 46 | padding: 1em 2em; 47 | 48 | > h3 { 49 | margin: 1em 0 2em; 50 | } 51 | 52 | > p { 53 | margin: 1em 0 3em; 54 | font-weight: bold; 55 | color: #888; 56 | letter-spacing: 0.5px; 57 | } 58 | `; 59 | 60 | export const TotalSection = styled.div` 61 | margin: 2em auto; 62 | font-size: 1.5em; 63 | padding: 0 3em; 64 | text-align: right; 65 | > span:first-child{ 66 | margin: 0 2em; 67 | } 68 | `; 69 | 70 | /** 71 | * Cart Item styled components 72 | */ 73 | 74 | 75 | export const CartItemWrap = styled.div` 76 | display: flex; 77 | flex-direction: row; 78 | align-items: center; 79 | justify-content: flex-start; 80 | padding: 2em; 81 | margin-bottom: 1em; 82 | border-bottom: 1px solid #eee; 83 | `; 84 | 85 | export const ItemImage = styled.img` 86 | flex: 0 0 80px; 87 | width: 80px; 88 | height: 80px; 89 | margin: 0 2em; 90 | `; 91 | 92 | export const ItemTitle = styled.div` 93 | flex: 1 1 60%; 94 | text-align: left; 95 | font-size: 1.1em; 96 | margin: 0 1em; 97 | `; 98 | 99 | export const DeleteIconWrap = styled.div` 100 | flex: 0 0 50px; 101 | text-align: left; 102 | font-size: 20px; 103 | margin: 0 1em; 104 | `; 105 | 106 | export const SoldOutError = styled.p` 107 | color: red; 108 | font-size: 14px; 109 | margin: 1em auto; 110 | `; 111 | 112 | export const PriceofItem = styled.div` 113 | flex: 1 0 200px; 114 | > span{ 115 | margin: 0 1em; 116 | } 117 | > span:first-child{ 118 | color: #aaa; 119 | } 120 | `; 121 | 122 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Category/CategoryItems.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types,react/forbid-prop-types */ 2 | import React, { Component } from 'react'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import styled from 'styled-components'; 6 | import { withRouter } from 'react-router-dom'; 7 | import CircularProgress from 'material-ui/CircularProgress'; 8 | 9 | import ProductItem from '../Product/ProductItem'; 10 | import SubCategories from './sub-categories'; 11 | import * as API from '../../service/grocery'; 12 | import ProductSkeleton from '../../base_components/ProductSkeleton'; 13 | 14 | const ItemsWrapper = styled.div` 15 | display: flex; 16 | flex-direction: row; 17 | flex-wrap: wrap; 18 | justify-content: flex-start; 19 | align-items: center; 20 | align-content: center; 21 | padding-bottom: 1em; 22 | margin: 1em auto; 23 | box-shadow: 0 0 26px 0 #eee; 24 | background: #eee; 25 | width: 75%; 26 | `; 27 | 28 | const Container = styled.div` 29 | display: flex; 30 | flex-direction: row; 31 | padding-bottom: 1em; 32 | margin: 1em auto; 33 | box-shadow: 0 0 26px 0 #eee; 34 | background: #eee; 35 | height: 100%; 36 | `; 37 | 38 | /** 39 | Display all the items for the particular category. 40 | having option to filter the items based on sub-categoryies. 41 | */ 42 | 43 | class CategoryItems extends Component { 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | items: [], 48 | subCategories: [], 49 | checked: {}, 50 | fetchingData: true, 51 | }; 52 | } 53 | 54 | componentDidMount() { 55 | if ( 56 | this.isValid(this.props.match) 57 | && this.isValid(this.props.match.params) 58 | && this.isValid(this.props.match.params.category) 59 | ) { 60 | const { category } = this.props.match.params; 61 | 62 | API.getCategoryGroceries(category).then((response) => { 63 | const { data } = response; 64 | const { Items } = data; 65 | let subCategories = Items.map(item => item.subCategory); 66 | subCategories = Array.from(new Set(subCategories)); 67 | const checked = {}; 68 | subCategories.map((cat) => { 69 | checked[cat] = true; 70 | return true; 71 | }); 72 | this.setState({ 73 | items: Items, 74 | subCategories, 75 | checked, 76 | fetchingData: false, 77 | }); 78 | }).catch(() => { 79 | this.setState({ fetchingData: false }); 80 | }); 81 | } else { 82 | this.props.history.push('/'); 83 | } 84 | } 85 | 86 | onCheck = (value) => { 87 | // const newValue = { [value]: !this.state.checked[value] }; 88 | this.setState({ 89 | checked: { ...this.state.checked, [value]: !this.state.checked[value] }, 90 | }); 91 | }; 92 | 93 | getItemsToShow = () => { 94 | const { checked, items } = this.state; 95 | let noItemAvailable = true; 96 | const categoryItems = items.map((item) => { 97 | if (checked[item.subCategory]) { 98 | noItemAvailable = false; 99 | return ( 100 | = 1} 108 | /> 109 | ); 110 | } 111 | return null; 112 | }); 113 | return { categoryItems, noItemAvailable }; 114 | }; 115 | 116 | isValid = st => !_.isEmpty(st) && !_.isNil(st); 117 | 118 | renderNoItems = () => { 119 | const { fetchingData } = this.state; 120 | const { categoryItems, noItemAvailable } = this.getItemsToShow(); 121 | 122 | if (noItemAvailable && !fetchingData) { 123 | return ( 124 |
125 | No items available. 126 |
127 | ); 128 | } 129 | if (!categoryItems && noItemAvailable && fetchingData) { 130 | return (); 131 | } 132 | return null; 133 | }; 134 | 135 | skeletons = () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => ); 136 | 137 | render() { 138 | const { subCategories, checked } = this.state; 139 | const { categoryItems, noItemAvailable } = this.getItemsToShow(); 140 | return ( 141 | 142 | 143 | { 144 | 149 | } 150 | 151 | { 152 | this.renderNoItems() 153 | } 154 | { 155 | this.state.items.length === 0 && !noItemAvailable 156 | && this.skeletons() 157 | } 158 | { 159 | !noItemAvailable && 160 | categoryItems 161 | } 162 | 163 | 164 | 165 | ); 166 | } 167 | } 168 | 169 | CategoryItems.propTypes = { 170 | match: PropTypes.shape({ 171 | params: PropTypes.shape({ 172 | category: PropTypes.string.isRequired, 173 | }), 174 | }).isRequired, 175 | location: PropTypes.object.isRequired, 176 | history: PropTypes.object.isRequired, 177 | }; 178 | 179 | export default withRouter(CategoryItems); 180 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Category/sub-categories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Checkbox from 'material-ui/Checkbox'; 4 | 5 | const Container = styled.div` 6 | width: 25%; 7 | margin: 2em 1em; 8 | background: #fff; 9 | padding: 2em 1em; 10 | height: 100%; 11 | box-shadow: 0px 0px 10px 0px #ddd; 12 | `; 13 | 14 | const SubCategorySkeleton = styled.div` 15 | margin: 1em 0em; 16 | width: 100%; 17 | background: #fff; 18 | background-repeat: no-repeat; 19 | background-image: linear-gradient(90deg,rgba(243,243,243,0) 0,rgba(243,243,243,0.4) 50%, rgba(243,243,243,0) 100%), linear-gradient(#f3f3f3 30px,transparent 0), linear-gradient(#f3f3f3 31px,transparent 0); 20 | background-size: 100px 100%, 30px 50px, 50% 20px; 21 | background-position: -150% 0, 5px 5px, 40% 10px; 22 | height: 40px; 23 | animation: loadingSubCat 2s infinite; 24 | `; 25 | 26 | /** 27 | Checkboxes with sub-categories to filter items in category page. 28 | */ 29 | 30 | export default ({ subCategories, checked, onCheck }) => ( 31 | 32 | { 33 | subCategories.length === 0 && 34 | [1, 2, 3, 4].map(val => ( 35 | 36 |   37 | 38 | )) 39 | } 40 | { 41 | subCategories.map((value, index) => { 42 | const label = value.charAt(0).toUpperCase() + value.slice(1); 43 | return ( 44 |
52 | (onCheck(value))} 56 | style={{ marginBottom: 0 }} 57 | labelStyle={{ textAlign: 'left', marginLeft: '5%' }} 58 | /> 59 |
60 | ); 61 | }) 62 | } 63 |
64 | ); 65 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Product/ProductHome.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import { Wrapper } from '../../base_components/index'; 5 | import ProductRow from './ProductRow'; 6 | import * as API from '../../service/grocery'; 7 | 8 | /** 9 | Home page to show top three items from each categories. 10 | having options to add items to the cart and 11 | go to the particular category to see all the items. 12 | */ 13 | 14 | class ProductHome extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | catData: null, 19 | error: null, 20 | }; 21 | } 22 | 23 | componentDidMount() { 24 | API.getTop3Groceries() 25 | .then((res) => { 26 | this.setState((state, props) => ({ 27 | catData: res.data, 28 | })); 29 | }) 30 | .catch((err) => { 31 | this.setState((state, props) => ({ 32 | error: err, 33 | })); 34 | }); 35 | } 36 | 37 | showErrorMessage = () => { 38 | if (this.state.error) { 39 | return ( 40 |
{JSON.stringify(this.state.error)}
41 | ); 42 | } 43 | return null; 44 | }; 45 | 46 | render() { 47 | const { catData } = this.state; 48 | return ( 49 | 50 | 51 | { 52 | catData && 53 | _.map(catData, (obj) => { 54 | const title = obj.category; 55 | const items = obj.groceries; 56 | return ( 57 | ); 62 | }) 63 | } 64 | 65 | { 66 | this.showErrorMessage() 67 | } 68 | 69 | 70 | ); 71 | } 72 | } 73 | 74 | ProductHome.propTypes = {}; 75 | 76 | export default ProductHome; 77 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Product/ProductItem.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | import _ from 'lodash'; 6 | import { connect } from 'react-redux'; 7 | import { bindActionCreators } from 'redux'; 8 | import { Card, CardActions, CardTitle, FlatButton } from 'material-ui'; 9 | 10 | import { pink500, pink800, pinkA200 } from 'material-ui/styles/colors'; 11 | import Quantity from '../../base_components/Quantity'; 12 | import ProductImageWrap from '../../base_components/ProductImage'; 13 | import { updateCartItems } from '../../actions/cart'; 14 | 15 | const ItemWrap = styled(Card)` 16 | box-shadow: none !important; 17 | margin: 1em 0.5em; 18 | overflow: hidden; 19 | border-radius: 4px; 20 | text-align: left; 21 | position: relative; 22 | width: 270px; 23 | border: 1px solid transparent; 24 | &:hover{ 25 | border: 1px solid #eee; 26 | box-shadow: 1px 3px 4px 0px rgba(144,144,144,0.44), 0px 0px 2px rgba(144,144,144,1) !important; 27 | } 28 | `; 29 | 30 | const AddCart = styled(FlatButton)` 31 | &:hover{ 32 | ${props => (!props.disabled ? ` 33 | color: #fff !important; 34 | font-weight: bold; 35 | background-color: ${pinkA200} !important; 36 | ` : '')} 37 | > div{ 38 | color: #fff !important; 39 | > span{ 40 | color: #fff !important; 41 | } 42 | } 43 | } 44 | font-size: 0.9em; 45 | `; 46 | 47 | const soldOutColor = pink500; 48 | 49 | const SoldOut = styled.span` 50 | background: ${soldOutColor}; 51 | color: #fff; 52 | position: absolute; 53 | z-index: 2; 54 | padding: 8px; 55 | margin: 0 auto; 56 | top: 0; 57 | left: 0; 58 | width: calc(100% + 16px); 59 | transform: translate(-16px, 0%); 60 | &:after, &:before{ 61 | content: ''; 62 | position: absolute; 63 | top: 99%; 64 | left: 0; 65 | border: solid transparent; 66 | 67 | } 68 | &:after{ 69 | border-width: 8px; 70 | border-right-color: ${soldOutColor}; 71 | border-top-color: ${soldOutColor}; 72 | } 73 | `; 74 | 75 | const CrossSoldOut = styled.span` 76 | background: ${soldOutColor}; 77 | color: #fff; 78 | position: absolute; 79 | z-index: 1; 80 | padding: 15px; 81 | margin: 0 auto; 82 | top: 20%; 83 | left: 0px; 84 | width: calc(140% + 10px); 85 | -webkit-transform: translate(-16px,0%); 86 | -ms-transform: translate(-16px,0%); 87 | transform: translate(-12%,40%) rotate(40deg); 88 | text-align: center; 89 | `; 90 | 91 | /** 92 | Individual product-item with image, name, price and option to add it to cart. 93 | */ 94 | 95 | class ProductItem extends Component { 96 | constructor(props) { 97 | super(props); 98 | this.state = { 99 | quantity: 1, 100 | }; 101 | } 102 | 103 | saveToCart = () => { 104 | this.props.updateCartItems(this.props.groceryId, this.state.quantity); 105 | }; 106 | 107 | displaySoldOut = () => { 108 | const { issoldout } = this.props; 109 | 110 | if (issoldout) { 111 | return ( 112 | 113 | Sold out 114 | ); 115 | } 116 | return null; 117 | }; 118 | 119 | displayQuantityCounter = (max) => { 120 | const { issoldout } = this.props; 121 | 122 | if (!issoldout) { 123 | return ( 124 | this.setState({ quantity: data })} 127 | initialQuantity={this.state.quantity} 128 | maxQuantity={max} 129 | disabled={issoldout} 130 | />); 131 | } 132 | return null; 133 | }; 134 | 135 | render() { 136 | const { 137 | name, price, url, issoldout, 138 | } = this.props; 139 | return ( 140 | 145 | {this.displaySoldOut()} 146 | 147 | 148 | 149 | 162 | 163 | 171 | { 172 | this.displayQuantityCounter(this.props.quant) 173 | } 174 | 175 | 186 | 187 | 188 | 189 | ); 190 | } 191 | } 192 | 193 | ProductItem.defaultProps = { 194 | issoldout: false, 195 | }; 196 | 197 | 198 | ProductItem.propTypes = { 199 | groceryId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, 200 | quant: PropTypes.number.isRequired, 201 | name: PropTypes.string.isRequired, 202 | price: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, 203 | url: PropTypes.string.isRequired, 204 | issoldout: PropTypes.bool, 205 | updateCartItems: PropTypes.func.isRequired, 206 | }; 207 | 208 | function initMapDispatchToProps(dispatch) { 209 | return bindActionCreators({ 210 | updateCartItems, 211 | }, dispatch); 212 | } 213 | 214 | export default connect(null, initMapDispatchToProps)(ProductItem); 215 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/Product/ProductRow.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import React from 'react'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import styled from 'styled-components'; 6 | import { Link } from 'react-router-dom'; 7 | import { pinkA200 } from 'material-ui/styles/colors'; 8 | 9 | import ProductItem from './ProductItem'; 10 | import { toProperCase } from '../../utils/string'; 11 | 12 | const RowWrapper = styled.div` 13 | margin-bottom: 1em; 14 | `; 15 | 16 | const ItemsWrapper = styled.div` 17 | display: flex; 18 | flex-direction: row; 19 | flex-wrap: wrap; 20 | justify-content: flex-start; 21 | align-items: flex-start; 22 | align-content: flex-start; 23 | padding: 0 1em 1em; 24 | margin: 1em auto 5em; 25 | box-shadow: 0 0 26px 0 #eee; 26 | background: #eee; 27 | 28 | @media (max-width: 922px) { 29 | justify-content: space-evenly; 30 | } 31 | `; 32 | 33 | const ProductTitle = styled.h1` 34 | color: #4f4d4d; 35 | letter-spacing: 0.5px; 36 | padding: 1em 8px; 37 | `; 38 | 39 | const MoreText = styled.span` 40 | display: inline-block; 41 | float: right; 42 | font-size: 16px; 43 | > a { 44 | color: ${pinkA200} 45 | } 46 | `; 47 | 48 | /** 49 | Row containing product items with link to category page. 50 | */ 51 | 52 | const ProductRow = ({ title, items }) => ( 53 | 54 | 55 | {toProperCase(title)} 56 | 57 | More ⟶ 58 | 59 | 60 | 61 | { 62 | _.map(items, obj => ( 63 | = 1} 71 | /> 72 | )) 73 | } 74 | 75 | 76 | ); 77 | 78 | 79 | ProductRow.propTypes = { 80 | title: PropTypes.string.isRequired, 81 | items: PropTypes.array.isRequired, 82 | }; 83 | 84 | export default ProductRow; 85 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/ProfileHome.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | import { RaisedButton } from 'material-ui'; 6 | import _ from 'lodash'; 7 | import {profileHomeSelector} from '../selectors/profile-home'; 8 | 9 | import { Wrapper } from '../base_components'; 10 | 11 | const Heading = styled.h1` 12 | padding: 0 1em 1em; 13 | border-bottom: 1px solid #eee; 14 | margin-bottom: 2em; 15 | `; 16 | 17 | const Field = styled.div` 18 | padding: 1em; 19 | line-height: 30px; 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: flex-start; 23 | align-items: center; 24 | 25 | > input.profileInput, .profileInput { 26 | padding: 1em 2em; 27 | flex: 1 1 20%; 28 | border-radius: 30px; 29 | color: #555; 30 | font-size: 16px; 31 | border: 1px solid #ddd; 32 | } 33 | 34 | > span:first-child{ 35 | flex: 0 0 150px; 36 | font-weight: bold; 37 | letter-spacing: 0.3px; 38 | padding-right: 10px; 39 | display: inline-block; 40 | text-align: left; 41 | } 42 | 43 | > span.profileInputDisabled{ 44 | flex: 1 1 20%; 45 | background: #eee; 46 | padding: 0.5em 2em; 47 | border-radius: 30px; 48 | color: #aaa; 49 | border: 1px solid #ddd; 50 | } 51 | `; 52 | 53 | const Verified = styled.span` 54 | flex: 0 0 30px; 55 | float: right; 56 | font-size: 16px; 57 | padding: 5px 8px; 58 | //background: ${props => (props.isVerified ? '#bbffbd' : '#ffc3bd')}; 59 | color: ${props => (props.isVerified ? '#18c532' : 'darkred')}; 60 | border: 1px solid ${props => (props.isVerified ? '#70b870' : '#850000')}; 61 | border-radius: 15px; 62 | margin-left: 1em; 63 | `; 64 | 65 | /** 66 | Profile page with user info: Name, email, phoneNumber, 67 | and button to save the changes in name/phoneNumber. 68 | */ 69 | 70 | class ProfileHome extends Component { 71 | constructor(props) { 72 | super(props); 73 | this.state = { 74 | fullName: this.props.name, 75 | phoneNumber: this.props.phoneNumber, 76 | }; 77 | } 78 | 79 | handleChange = (e) => { 80 | if (!_.isEmpty(e.target) && !_.isEmpty(e.target.name)) { 81 | this.setState({ 82 | [e.target.name]: e.target.value, 83 | }); 84 | } 85 | }; 86 | 87 | saveProfile = () => { 88 | const { fullName, phoneNumber } = this.state; 89 | alert(fullName + phoneNumber); 90 | }; 91 | 92 | 93 | render() { 94 | const { fullName, phoneNumber } = this.state; 95 | const { attributes } = this.props.userData; 96 | const { 97 | isPhoneNumberEmpty, 98 | isFullNameEmpty, 99 | name, 100 | email, 101 | emailVerified, 102 | phoneNumberVerified 103 | } = this.props; 104 | const saveDisabled = ((isFullNameEmpty && isPhoneNumberEmpty)) 105 | || (_.isEqual(fullName, name) && _.isEqual(phoneNumber, this.props.phoneNumber)); 106 | return ( 107 | 115 | 116 | My Profile 117 | 118 | 119 | Name: 120 | 128 | 129 | 130 | Email: 131 | 132 | {email} 133 | 134 | 138 | verified_user 139 | 140 | 141 | 142 | Phone Number: 143 | 150 | verified_user 154 | 155 | 156 | 157 | 168 | 169 | 170 | ); 171 | } 172 | } 173 | 174 | ProfileHome.defaultProps = { 175 | userData: {}, 176 | }; 177 | 178 | 179 | ProfileHome.propTypes = { 180 | isPhoneNumberEmpty: PropTypes.bool.isRequired, 181 | isFullNameEmpty: PropTypes.bool.isRequired, 182 | phoneNumber: PropTypes.string.isRequired, 183 | name: PropTypes.string.isRequired, 184 | email: PropTypes.string.isRequired, 185 | emailVerified: PropTypes.bool.isRequired, 186 | phoneNumberVerified: PropTypes.bool.isRequired, 187 | }; 188 | 189 | function initMapStateToProps(state) { 190 | const { 191 | isPhoneNumberEmpty, 192 | isFullNameEmpty, 193 | phoneNumber, 194 | name, 195 | email, 196 | emailVerified, 197 | phoneNumberVerified 198 | } = profileHomeSelector(state); 199 | return { 200 | isPhoneNumberEmpty, 201 | isFullNameEmpty, 202 | phoneNumber, 203 | name, 204 | email, 205 | emailVerified, 206 | phoneNumberVerified 207 | }; 208 | } 209 | 210 | function initMapDispatchToProps() { 211 | 212 | } 213 | 214 | export default connect(initMapStateToProps, initMapDispatchToProps)(ProfileHome); 215 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/header.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import styled from 'styled-components'; 5 | import { Link } from 'react-router-dom'; 6 | import { bindActionCreators } from 'redux'; 7 | import PropTypes from 'prop-types'; 8 | 9 | import { FlatButton, IconButton } from 'material-ui'; 10 | import { Auth } from 'aws-amplify'; 11 | import AppBar from 'material-ui/AppBar'; 12 | import { updateAuth } from '../Auth/actionCreators'; 13 | import { fetchCartItems } from '../actions/cart'; 14 | import { fetchAllOrders } from '../actions/order'; 15 | import { headerSelector } from '../selectors/header' 16 | 17 | const AppHeader = styled(AppBar)` 18 | position: fixed; 19 | top: 0; 20 | align-items: flex-start; 21 | `; 22 | 23 | const RightElementContainer = styled.div` 24 | display: flex; 25 | height: 100%; 26 | align-items: center; 27 | flex-direction: row; 28 | justify-content: space-between; 29 | `; 30 | 31 | const LogoutButton = styled(FlatButton)` 32 | color: white !important; 33 | `; 34 | 35 | const CartItemsCount = styled.div` 36 | align-items: center; 37 | background-color: white; 38 | border-radius: 15px; 39 | display: flex; 40 | font-size: 11px; 41 | height: 20px; 42 | justify-content: center; 43 | position: absolute; 44 | right:-1em; 45 | top: 0px; 46 | color: #000; 47 | width: 30px; 48 | `; 49 | 50 | /** 51 | Header having home icon, title of the application, cartItems count 52 | and logout option. 53 | */ 54 | 55 | class Header extends React.Component { 56 | constructor(props) { 57 | super(props); 58 | this.handleLogout = this.handleLogout.bind(this); 59 | } 60 | 61 | componentDidMount() { 62 | const {orderListFetched, fetchAllOrders, fetchCartItems} = this.props; 63 | if (!orderListFetched) { 64 | fetchAllOrders(); 65 | } 66 | fetchCartItems(); 67 | } 68 | 69 | async handleLogout() { 70 | await Auth.signOut(); 71 | this.resetInitialState(); 72 | } 73 | 74 | resetInitialState = () => { 75 | this.props.updateAuth({ 76 | isAuthenticating: false, 77 | isAuthenticated: false, 78 | identityId: null, 79 | userData: null, 80 | }); 81 | }; 82 | 83 | renderLeftIcons = () => ( 84 | 85 | 86 | home 87 | 88 | 89 | ) 90 | 91 | renderRightIcons = () => ( 92 | 93 | 100 | 106 | add_shopping_cart 107 | 108 | { 109 | !this.props.isCartDataEmpty? {this.props.cartDataLength} : null 110 | } 111 | 112 | 120 | Order List 121 | 122 | 123 | 124 | ) 125 | 126 | render() { 127 | return ( 128 | Serverless Shopping App} 130 | iconElementLeft={this.renderLeftIcons()} 131 | iconElementRight={this.renderRightIcons()} 132 | /> 133 | ); 134 | } 135 | } 136 | 137 | Header.propTypes = { 138 | cartDataLength: PropTypes.number.isRequired, 139 | isCartDataEmpty: PropTypes.bool.isRequired, 140 | orderListFetched: PropTypes.bool.isRequired, 141 | }; 142 | 143 | const mapStateToProps = state => { 144 | const {orderListFetched, cartDataLength, isCartDataEmpty} = headerSelector(state); 145 | return ({ 146 | orderListFetched, 147 | cartDataLength, 148 | isCartDataEmpty 149 | }); 150 | }; 151 | 152 | const mapDispatchToProps = dispatch => ({ 153 | updateAuth: bindActionCreators(updateAuth, dispatch), 154 | fetchCartItems: bindActionCreators(fetchCartItems, dispatch), 155 | fetchAllOrders: bindActionCreators(fetchAllOrders, dispatch), 156 | }); 157 | 158 | export default connect(mapStateToProps, mapDispatchToProps)(Header); 159 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/order-list/details.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | import Dialog from 'material-ui/Dialog'; 5 | import isEmpty from 'lodash/isEmpty'; 6 | 7 | const List = styled.div` 8 | display: flex; 9 | flex-direction: row; 10 | width: 100%; 11 | justify-content: space-between; 12 | padding-bottom: 2%; 13 | overflow: auto; 14 | `; 15 | 16 | const Section = styled.div` 17 | display: flex; 18 | flex-direction: row; 19 | `; 20 | 21 | const Item = styled.p` 22 | color: #686b78; 23 | `; 24 | 25 | const ListContainer = styled.div` 26 | padding-top: 3%; 27 | `; 28 | 29 | const TitleContainer = styled.div` 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: space-between; 33 | `; 34 | 35 | /** 36 | Modal to show the details of the order. 37 | Having option to pay for the order, if in Pending state. 38 | */ 39 | 40 | class OrderDetails extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | } 45 | } 46 | 47 | title = ({label, orderId, onSubmit}) => ( 48 | 49 |
50 | {orderId} 51 |
52 | { 53 | !isEmpty(this.props.order) && 54 | this.props.order.orderStatus === 'PAYMENT_PENDING' && 55 | 61 | } 62 |
63 | ); 64 | 65 | renderTotal = ({orderTotal})=> ( 66 | 67 |
68 | Total 69 |
70 |
71 |

₹{` ${orderTotal}`} 

72 |
73 |
74 | ) 75 | 76 | renderListItem = ({index, name, qty, price}) => ( 77 | 78 |
79 | {name} 80 |  {`x ${qty}`}  81 |
82 |
83 |

₹{` ${price}`} 

84 |
85 |
86 | ) 87 | 88 | render() { 89 | const {openDialog, closeDialog, openStripePaymentModal, paymentInProgress, order} = this.props; 90 | const {orderItems, orderTotal, orderId} = order; 91 | return ( 92 | 102 | 103 | { 104 | !isEmpty(orderItems) && orderItems.map(({ name, qty, price }, index) => ( 105 | this.renderListItem({index, name, qty, price}) 106 | )) 107 | } 108 |
109 | {this.renderTotal({orderTotal})} 110 |
111 |
112 | ); 113 | } 114 | } 115 | 116 | 117 | export default OrderDetails; 118 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/order-list/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import styled from 'styled-components'; 4 | import { bindActionCreators } from 'redux'; 5 | import { withRouter } from 'react-router-dom'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import OrderDetails from './details'; 8 | import { submitPaymentTokenId, clearPayment } from '../../actions/payment'; 9 | import {displayPaymentModal} from '../../utils/stripe-payment-modal'; 10 | import styles from './styles.css'; 11 | import {orderListSelector} from '../../selectors/order-list'; 12 | 13 | import sortBy from 'lodash/sortBy'; 14 | 15 | const NoOrder = styled.div` 16 | height: 100px; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | `; 21 | 22 | const Card = styled.div` 23 | position: relative; 24 | max-width: 700px; 25 | width: 90%; 26 | height: 100px; 27 | background: #fff; 28 | box-shadow: 0 0 15px rgba(0,0,0,.1); 29 | margin: 2% auto; 30 | padding: 1% 2%; 31 | border: 1px solid #E3DFDE; 32 | color: '#393736'; 33 | `; 34 | 35 | const Content = styled.div` 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: space-between; 39 | width: 100%; 40 | `; 41 | 42 | const IconContainer = styled.div` 43 | width: 30px; 44 | height: 30px; 45 | border-radius: 50%; 46 | background-color: #66b34d; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | `; 51 | 52 | const AmountContainer = styled.div` 53 | padding-top: 2%; 54 | text-align: left; 55 | `; 56 | 57 | const ButtonContainer = styled.div` 58 | text-align: left; 59 | `; 60 | 61 | const Icon = styled.i` 62 | font-size: 20px; 63 | color: white; 64 | `; 65 | 66 | const pendingConfig = { 67 | statusText: "Pending", 68 | statusColor: '#ecb613', 69 | payText: "Pay", 70 | cssStyle: 'order-pending', 71 | textColor: '#dfaf20' 72 | }; 73 | 74 | const completedConfig = { 75 | statusText: "Completed", 76 | statusColor: '#66b34d', 77 | payText: "Paid", 78 | cssStyle: 'order-complete', 79 | textColor: '#69ac53' 80 | }; 81 | 82 | const canceledConfig = { 83 | statusText: "Canceled", 84 | statusColor: '#e64d19', 85 | payText: "Amount", 86 | cssStyle: 'order-canceled', 87 | textColor: '#df5020' 88 | } 89 | 90 | /** 91 | List of all the order placed, canceled or pending. 92 | On click orderId, open the modal to show details of that order. 93 | Option for payment if order is in Pending state. 94 | */ 95 | 96 | class OrderList extends React.Component { 97 | constructor(props) { 98 | super(props); 99 | this.state = { 100 | selectedOrder: {}, 101 | openDialog: false, 102 | orderTotal: null, 103 | } 104 | } 105 | 106 | componentWillReceiveProps(nextProps) { 107 | if (nextProps.paymentComplete) { 108 | this.closeDialog(); 109 | nextProps.clearPayment(); 110 | } 111 | } 112 | 113 | onSelect = (selectedOrder) => { 114 | this.setState({ 115 | selectedOrder, 116 | openDialog: true 117 | }); 118 | } 119 | 120 | closeDialog = () => { 121 | this.setState({ 122 | selectedOrder: {}, 123 | openDialog: false 124 | }); 125 | } 126 | 127 | onClosePaymentModal = () => { 128 | this.props.history.push('/order-list'); 129 | } 130 | 131 | openStripePaymentModal = () => { 132 | displayPaymentModal( 133 | this.props, 134 | null, 135 | this.onClosePaymentModal, 136 | this.props.submitPaymentTokenId 137 | ) 138 | } 139 | 140 | renderNoOrder = () => ( 141 | 142 | There is no order placed yet. 143 | 144 | ) 145 | 146 | renderRibbon = ({cssStyle, statusColor}) => ( 147 |
148 | NEW 149 |
150 | ) 151 | 152 | renderContent = ({orderId, orderTotal, orderItems, orderStatus, statusText, textColor, statusColor}) => ( 153 | 154 |
this.onSelect({orderId, orderTotal, orderItems, orderStatus})} 156 | style={{ 157 | cursor: 'pointer', 158 | color: textColor, 159 | marginBottom: '2%' 160 | }}> 161 | {`OrderId: ${orderId}`} 162 |
163 | { 164 | statusText === "Completed" ? 165 | 166 | done 167 | : 168 |
169 | {statusText} 170 |
171 | } 172 |
173 | ); 174 | 175 | renderButton = ({payText, orderTotal}) => ( 176 | 177 | 187 | 188 | ); 189 | 190 | renderAmountText = ({textColor, orderTotal, payText}) => ( 191 | 192 | {payText}: ₹{` ${orderTotal}`} 193 | 194 | ) 195 | 196 | renderOrderCard = ({orderId, orderItems, orderStatus, orderTotal, orderDate}, index) => { 197 | const timeStamp = (new Date(orderDate)).getTime(); 198 | const inMinutes = (Date.now() - timeStamp) / (60000); 199 | const { 200 | statusText, 201 | statusColor, 202 | payText, 203 | cssStyle, 204 | textColor 205 | } = (orderStatus === 'PAYMENT_PENDING'? pendingConfig : ( 206 | orderStatus === 'CANCELLED'? canceledConfig : completedConfig)); 207 | return ( 208 | 209 | { inMinutes < 5 && this.renderRibbon({cssStyle, statusColor}) } 210 | {this.renderContent({orderId, orderTotal, orderItems, orderStatus, textColor, statusColor, statusText})} 211 | { 212 | orderStatus === 'PAYMENT_PENDING' ? 213 | (this.state.openDialog? null : this.renderButton({payText, orderTotal})) : 214 | this.renderAmountText({textColor, orderTotal, payText}) 215 | } 216 | 217 | ); 218 | } 219 | 220 | render() { 221 | let {orderList, orderListFetched, isOrderlistEmpty} = this.props; 222 | if (!orderListFetched) { 223 | return null; 224 | } 225 | return ( 226 |
227 | { 228 | isOrderlistEmpty? 229 | this.renderNoOrder(): 230 |
231 | { 232 | orderList.map((item, index) => { 233 | return (this.renderOrderCard(item, index)); 234 | }) 235 | } 236 |
237 | } 238 | 245 |
246 | ); 247 | } 248 | } 249 | 250 | const mapStateToProps = state => { 251 | const { 252 | orderList, 253 | isOrderlistEmpty, 254 | orderListFetched, 255 | orderTotal, 256 | orderId, 257 | username, 258 | paymentInProgress, 259 | paymentComplete 260 | } = orderListSelector(state); 261 | return ({ 262 | orderList, 263 | isOrderlistEmpty, 264 | orderListFetched, 265 | orderTotal, 266 | orderId, 267 | username, 268 | paymentInProgress, 269 | paymentComplete 270 | }); 271 | } 272 | 273 | function initMapDispatchToProps(dispatch) { 274 | return bindActionCreators({ 275 | submitPaymentTokenId, 276 | clearPayment 277 | }, dispatch); 278 | } 279 | 280 | export default connect(mapStateToProps, initMapDispatchToProps)(OrderList); 281 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/order-list/styles.css: -------------------------------------------------------------------------------- 1 | .box { 2 | position: relative; 3 | max-width: 700px; 4 | width: 90%; 5 | height: 100px; 6 | background: #fff; 7 | box-shadow: 0 0 15px rgba(0,0,0,.1); 8 | margin: 2% auto; 9 | padding: 1% 2%; 10 | border: 1px solid #E3DFDE; 11 | color: '#393736'; 12 | } 13 | 14 | .ribbon { 15 | width: 73px; 16 | height: 73px; 17 | overflow: hidden; 18 | position: absolute; 19 | } 20 | 21 | .ribbon::before, 22 | .ribbon::after { 23 | position: absolute; 24 | z-index: -1; 25 | content: ''; 26 | display: block; 27 | } 28 | 29 | .order-pending::before, 30 | .order-pending::after { 31 | border: 5px solid #cca633; 32 | } 33 | 34 | .order-complete::before, 35 | .order-complete::after { 36 | border: 5px solid #709f60; 37 | } 38 | 39 | .order-canceled::before, 40 | .order-canceled::after { 41 | border: 5px solid #cc5933; 42 | } 43 | 44 | .ribbon span { 45 | position: absolute; 46 | width: 100px; 47 | padding: 10px 0; 48 | background-color: #3498db; 49 | color: #fff; 50 | text-align: center; 51 | font-size: 8px; 52 | } 53 | 54 | .ribbon-top-right { 55 | top: -10px; 56 | right: -10px; 57 | } 58 | .ribbon-top-right::before, 59 | .ribbon-top-right::after { 60 | border-top-color: transparent; 61 | border-right-color: transparent; 62 | } 63 | .ribbon-top-right::before { 64 | top: 0; 65 | left: 8px; 66 | } 67 | .ribbon-top-right::after { 68 | bottom: 8px; 69 | right: 0; 70 | } 71 | .ribbon-top-right span { 72 | left: 0px; 73 | top: 7px; 74 | transform: rotate(45deg); 75 | } 76 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/components/order-placed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import styled from 'styled-components'; 5 | import { bindActionCreators } from 'redux'; 6 | import { withRouter } from 'react-router-dom'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import { cleanCart } from '../actions/cart'; 9 | import { cleanOrder } from '../actions/order'; 10 | import isEmpty from 'lodash/isEmpty'; 11 | 12 | const Container = styled.div` 13 | margin: 10% auto; 14 | width: 50%; 15 | border: 2px solid #669980; 16 | border-radius: 5px; 17 | padding-top: 2%; 18 | padding-bottom: 2%; 19 | background-color: white; 20 | `; 21 | 22 | const Heading = styled.div` 23 | font-weight: bold; 24 | font-size: 18px; 25 | color: #4db380; 26 | `; 27 | 28 | const OrderId = styled.div` 29 | font-weight: bold; 30 | font-size: 18px; 31 | color: #686b78; 32 | margin-top: 2%; 33 | `; 34 | 35 | const ListContainer = styled.div` 36 | padding: 5% 10%; 37 | `; 38 | 39 | const List = styled.div` 40 | display: flex; 41 | flex-direction: row; 42 | width: 100%; 43 | font-weight: bold; 44 | font-size: 16px; 45 | justify-content: space-between; 46 | padding-bottom: 2%; 47 | `; 48 | 49 | const Section = styled.div` 50 | display: flex; 51 | flex-direction: row; 52 | `; 53 | 54 | const Item = styled.p` 55 | color: #686b78; 56 | `; 57 | 58 | class OrderPlaced extends React.Component { 59 | constructor(props) { 60 | super(props); 61 | this.state = { 62 | cartItems: [], 63 | orderId: null, 64 | }; 65 | } 66 | 67 | componentWillMount() { 68 | const { cartItems, currentOrder } = this.props; 69 | if (isEmpty(cartItems) || isEmpty(currentOrder)) { 70 | // this.props.cleanCart(); 71 | this.props.cleanOrder(); 72 | this.props.history.push('/'); 73 | return; 74 | } 75 | this.setState({ 76 | cartItems: this.props.cartItems, 77 | orderId: this.props.currentOrder.orderId, 78 | }, () => { 79 | this.props.cleanCart(); 80 | this.props.cleanOrder(); 81 | }); 82 | } 83 | 84 | render() { 85 | const { cartItems, orderId } = this.state; 86 | let totalAmount = 0; 87 | return ( 88 | 89 | 90 | Thanks for Shopping with us. 91 | 92 | 93 | {`Order-Id ${orderId}`} 94 | 95 | 96 | { 97 | cartItems.length > 0 && 98 | cartItems.map(({ name, boughtQty, price }, index) => { 99 | totalAmount += price; 100 | return ( 101 | 102 |
103 | {name} 104 |  {`x ${boughtQty}`}  105 |
106 |
107 |

₹{` ${price}`} 

108 |
109 |
110 | ); 111 | }) 112 | } 113 |
114 | 115 |
116 | Total 117 |
118 |
119 |

₹{` ${totalAmount}`} 

120 |
121 |
122 |
123 | this.props.history.push('/')} 128 | /> 129 |
130 | ); 131 | } 132 | } 133 | 134 | const mapStateToProps = state => ({ 135 | cartItems: state.cart.cartItemsInfo, 136 | currentOrder: state.order.currentOrder, 137 | }); 138 | 139 | const mapDispatchToProps = dispatch => ({ 140 | cleanCart: bindActionCreators(cleanCart, dispatch), 141 | cleanOrder: bindActionCreators(cleanOrder, dispatch), 142 | }); 143 | 144 | export default connect(mapStateToProps, mapDispatchToProps)(withRouter(OrderPlaced)); 145 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/constants/app.js: -------------------------------------------------------------------------------- 1 | export const USER_NOT_VERIFIED = 'USER_NOT_VERIFIED'; 2 | export const USER_ALREADY_EXIST = 'USER_ALREADY_EXIST'; 3 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | background: #f5f5f5; 15 | font-family: 'Roboto', sans-serif; 16 | box-sizing: border-box; 17 | } 18 | 19 | .error-message { 20 | color: #721c24; 21 | background-color: #f8d7da; 22 | padding: 1em 5em; 23 | border: 1px solid #f5c6cb; 24 | margin: 1em auto; 25 | border-radius: 3em; 26 | } 27 | 28 | @keyframes loading { 29 | to { 30 | background-position: 350% 0, 50% 20px, 31 | 50% 270px, 1em 310px, 90% 345px, 32 | 0% 0%; 33 | } 34 | } 35 | 36 | @keyframes loadingCart { 37 | to { 38 | background-position: 300% 0, 64px 32px, 30% 60px, 76% 50px, 85% 50px, 39 | 0% 0%; 40 | } 41 | } 42 | @keyframes loadingSubCat { 43 | to { 44 | background-position: 150% 0, 5px 5px, 40% 10px; 45 | } 46 | } -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import Amplify from 'aws-amplify'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | 7 | import './index.css'; 8 | import './utils/string'; 9 | 10 | import registerServiceWorker from './registerServiceWorker'; 11 | import store from './store'; 12 | import Routes from './routes'; 13 | 14 | Amplify.configure({ 15 | Auth: { 16 | mandatorySignIn: true, 17 | region: process.env.REACT_APP_REGION, 18 | userPoolId: process.env.REACT_APP_USER_POOL_ID, 19 | identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID, 20 | userPoolWebClientId: process.env.REACT_APP_APP_CLIENT_ID, 21 | }, 22 | API: { 23 | endpoints: [ 24 | { 25 | name: 'groceryApp', 26 | endpoint: process.env.REACT_APP_URL, 27 | region: process.env.REACT_APP_REGION, 28 | }, 29 | ], 30 | }, 31 | }); 32 | 33 | const Index = () => ( 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | ReactDOM.render(, document.getElementById('root')); 42 | registerServiceWorker(); 43 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/reducers/cart.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | cartData: [], 3 | cartItemsInfo: [], 4 | inProgress: false 5 | }; 6 | 7 | /** 8 | Store the data for the cart and its details 9 | */ 10 | 11 | export default (state = initialState, { type, payload = {} }) => { 12 | switch (type) { 13 | case 'USER_CART_ITEMS': 14 | return { 15 | ...state, 16 | cartData: payload.cartData, 17 | }; 18 | case 'SAVE_NEW_CART': 19 | return { 20 | ...state, 21 | cartData: payload, 22 | }; 23 | case 'SAVE_ITEM_INFO': 24 | return { 25 | ...state, 26 | cartItemsInfo: payload, 27 | inProgress: false 28 | }; 29 | case 'SAVE_NEW_CART_INFO': 30 | return { 31 | ...state, 32 | cartItemsInfo: payload, 33 | }; 34 | case 'IN_PROGRESS': 35 | return { 36 | ...state, 37 | inProgress: true 38 | } 39 | default: 40 | return state; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/reducers/orders.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | currentOrder: null, 3 | orderList: [], 4 | orderListFetched: false, 5 | }; 6 | 7 | /** 8 | Store all order details in orderList and 9 | pending order in currentOrder 10 | */ 11 | 12 | export default (state = initialState, { type, payload = {}, ...rest }) => { 13 | switch (type) { 14 | case 'SAVE_ORDER_ID': 15 | return { 16 | ...state, 17 | currentOrder: payload, 18 | }; 19 | case 'SAVE_ALL_ORDERS': 20 | return { 21 | ...state, 22 | orderList: payload, 23 | currentOrder: rest.pendingOrder, 24 | orderListFetched: true 25 | }; 26 | case 'CLEAR_ORDER': 27 | return { 28 | ...state, 29 | currentOrder: [], 30 | orderList: [], 31 | }; 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/reducers/payment.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | paymentComplete: null, 3 | paymentError: null, 4 | paymentInProgress: null 5 | } 6 | 7 | /** 8 | Store the payment status: inProgress, completed or failed 9 | */ 10 | 11 | export default (state = initialState, { type, payload = {}}) => { 12 | switch(type) { 13 | case 'PAYMENT_IN_PROGRESS': 14 | return { 15 | ...state, 16 | paymentComplete: false, 17 | paymentInProgress: true, 18 | paymentError: null 19 | } 20 | case 'PAYMENT_SUCCESS': 21 | return { 22 | ...state, 23 | paymentComplete: true, 24 | paymentInProgress: false, 25 | paymentError: null 26 | } 27 | case 'PAYMENT_FAILURE': 28 | return { 29 | ...state, 30 | paymentComplete: false, 31 | paymentInProgress: false, 32 | paymentError: payload.error 33 | } 34 | case 'CLEAR_PAYMENT': 35 | return { 36 | ...state, 37 | paymentComplete: null, 38 | paymentInProgress: null, 39 | paymentError: null 40 | } 41 | default: 42 | return state; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import { Auth } from 'aws-amplify'; 4 | import { connect } from 'react-redux'; 5 | import { bindActionCreators } from 'redux'; 6 | 7 | import Header from './components/header'; 8 | import CategoryItems from './components/Category/CategoryItems'; 9 | import { fetchAllOrders } from './actions/order'; 10 | 11 | import AuthModule from './Auth'; 12 | import ProductHome from './components/Product/ProductHome'; 13 | import { updateAuth } from './Auth/actionCreators'; 14 | import CartHome from './components/Cart/CartHome'; 15 | import OrderPlaced from './components/order-placed'; 16 | import ProfileHome from './components/ProfileHome'; 17 | import BillReceipt from './components/Cart/BillReceipt'; 18 | import OrderList from './components/order-list'; 19 | 20 | const DefaultLayout = ({component: Component, ...rest}) => ( 21 | ( 24 |
25 |
26 | 27 |
28 | )} /> 29 | ); 30 | 31 | class Routes extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | loginReady: false, 36 | }; 37 | } 38 | 39 | async componentDidMount() { 40 | try { 41 | this.resetAndStartAuthentication(); 42 | Auth.currentSession().then(async (response) => { 43 | const data = await Auth.currentAuthenticatedUser(); 44 | const userData = await Auth.currentUserInfo(); 45 | this.props.updateAuth({ 46 | isAuthenticating: false, 47 | isAuthenticated: true, 48 | userData, 49 | identityId: data, 50 | }); 51 | this.props.fetchAllOrders(); 52 | }).catch((error) => { 53 | this.finishAuthentication(); 54 | }); 55 | } catch (e) { 56 | // to do 57 | } 58 | } 59 | 60 | componentWillReceiveProps(nextProps) { 61 | if (!this.state.loginReady && this.props.isAuthenticating && !nextProps.isAuthenticating) { 62 | this.setState({ loginReady: true }); 63 | } 64 | } 65 | 66 | resetAndStartAuthentication = () => { 67 | this.props.updateAuth({ 68 | isAuthenticating: true, 69 | isAuthenticated: false, 70 | identityId: null, 71 | userData: null, 72 | }); 73 | }; 74 | 75 | finishAuthentication = () => { 76 | this.props.updateAuth({ 77 | isAuthenticating: false, 78 | }); 79 | }; 80 | 81 | render() { 82 | return ( 83 | 84 |
85 | { 86 | !this.props.isAuthenticated && this.state.loginReady ? 87 | } /> 88 | : 89 | (this.state.loginReady ? 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | : 101 | null 102 | ) 103 | } 104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | const mapStateToProps = state => ({ 111 | isAuthenticated: state.auth.isAuthenticated, 112 | isAuthenticating: state.auth.isAuthenticating, 113 | identityId: state.auth.identityId, 114 | }); 115 | 116 | const mapDispatchToProps = dispatch => ({ 117 | updateAuth: bindActionCreators(updateAuth, dispatch), 118 | fetchAllOrders: bindActionCreators(fetchAllOrders, dispatch), 119 | }); 120 | 121 | export default connect(mapStateToProps, mapDispatchToProps)(Routes); 122 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/authenticationSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeLatest, 3 | put, 4 | call, 5 | } from 'redux-saga/effects'; 6 | import loginFailureSaga from './loginFailureSaga'; 7 | import registerSaga from './registerSaga'; 8 | import loginSaga from './loginSaga'; 9 | 10 | /** 11 | * Login Function called on latest 'ATTEMPT_LOGIN' actionType 12 | * @param action 13 | */ 14 | function* loginAttempt(action) { 15 | /** Send a new action to mark login/register as in-progress */ 16 | yield put({type: 'IN_PROGRESS'}); 17 | try { 18 | /** 19 | * action fired from register screen 20 | */ 21 | if (action.payload.authScreen === 'register') { 22 | const register = () => registerSaga(); 23 | yield call(register); 24 | } else { 25 | /** 26 | * action fired from login screen 27 | */ 28 | const login = () => loginSaga(); 29 | yield call(login); 30 | } 31 | } catch (e) { 32 | console.log(e); 33 | /** 34 | * trigger an saga in case login values due to any error 35 | */ 36 | const loginFail = (() => (loginFailureSaga({e, authScreen: action.payload.authScreen }))); 37 | yield call(loginFail); 38 | } 39 | } 40 | 41 | /** 42 | * Take Latest 'ATTEMPT_LOGIN' actionType and call the method 43 | */ 44 | function* authenticationSaga() { 45 | yield takeLatest('ATTEMPT_LOGIN', loginAttempt); 46 | } 47 | 48 | export default authenticationSaga; 49 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/forgotPasswordRequestSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeLatest, 3 | put, 4 | select, 5 | call, 6 | } from 'redux-saga/effects'; 7 | import { Auth } from 'aws-amplify'; 8 | import loginFailureSaga from './loginFailureSaga'; 9 | 10 | /** 11 | * Get values from forgotPassword form 12 | */ 13 | const forgotPasswordFormSelector = state => state.form.forgotPassword.values; 14 | 15 | /** 16 | * Promise returned by forgotPassword method of AWS Amplify 17 | */ 18 | const forgotPasswordRequest = ({username}) => Auth.forgotPassword(username); 19 | 20 | /** 21 | * Function called on receiving the action 22 | */ 23 | function* forgotPassword(action) { 24 | try { 25 | /** 26 | * Get username (email) value from the form 27 | */ 28 | const {username} = yield select(forgotPasswordFormSelector); 29 | /** 30 | * Make Request to Cognito to trigger a forgot password request 31 | */ 32 | yield call(forgotPasswordRequest, { username }); 33 | /** 34 | * Send an action to inform forgot password is in progress 35 | */ 36 | yield put({ 37 | type: 'FORGOT_PASSWORD_REQUESTED' 38 | }); 39 | } catch (e) { 40 | /** 41 | * Trigger saga incase any error encountered 42 | */ 43 | const loginFail = (() => (loginFailureSaga({e, authScreen: 'login' }))); 44 | yield call(loginFail); 45 | } 46 | } 47 | 48 | /** 49 | * Saga which takes latest actionType 'FORGOT_PASSWORD_REQUEST' 50 | */ 51 | function* forgotPasswordRequestSaga() { 52 | yield takeLatest('FORGOT_PASSWORD_REQUEST', forgotPassword); 53 | } 54 | 55 | export default forgotPasswordRequestSaga; 56 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/forgotPasswordSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeLatest, 3 | put, 4 | select, 5 | call, 6 | } from 'redux-saga/effects'; 7 | import { Auth } from 'aws-amplify'; 8 | import loginFailureSaga from './loginFailureSaga'; 9 | import loginSaga from './loginSaga'; 10 | 11 | /** 12 | * Get values from forgotPassword form 13 | */ 14 | const forgotPasswordFormSelector = state => state.form.forgotPassword.values; 15 | 16 | function* forgotPassword(action) { 17 | /** 18 | * Promise returned by Amplify library for forgotPassword 19 | * @param username {string} email of user 20 | * @param code {number} code sent on email of user, required to reset password 21 | * @param password {string} new password 22 | */ 23 | const forgotPasswordPromise = ({username, code, password}) => Auth.forgotPasswordSubmit(username, code, password); 24 | try { 25 | // send action to notify task in progess 26 | yield put({type: 'IN_PROGRESS'}); 27 | 28 | // Get the values from the forgotPassword form 29 | const {username, code, password} = yield select(forgotPasswordFormSelector) ; 30 | 31 | // Call the promise with the input values of form 32 | yield call(forgotPasswordPromise, { username, code, password}); 33 | 34 | // Sign the user in, once password change is successful 35 | const login = () => loginSaga(username, password); 36 | yield call(login); 37 | } catch (e) { 38 | console.log(e); 39 | // Call the saga in case any error occurs 40 | const loginFail = (() => (loginFailureSaga({e, authScreen: 'login' }))); 41 | yield call(loginFail); 42 | } 43 | } 44 | 45 | /** 46 | * Saga to call on action type 'FORGOT_PASSWORD' 47 | */ 48 | function* forgotPasswordSaga() { 49 | yield takeLatest('FORGOT_PASSWORD', forgotPassword); 50 | } 51 | 52 | export default forgotPasswordSaga; 53 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/loginFailureSaga.js: -------------------------------------------------------------------------------- 1 | import { put } from 'redux-saga/effects'; 2 | import { USER_NOT_VERIFIED, USER_ALREADY_EXIST } from '../../constants/app'; 3 | 4 | /** 5 | * Saga which handles the error on login/signup 6 | */ 7 | function* loginFailureSaga({e, authScreen}) { 8 | // check error message type 9 | let errorMessage = typeof e === 'string' && e; 10 | let userNotVerified = (e.code === 'UserNotConfirmedException')? USER_NOT_VERIFIED : null; 11 | 12 | // Check and switch based on different errors 13 | switch(e.code) { 14 | case 'UsernameExistsException': 15 | errorMessage = USER_ALREADY_EXIST; 16 | break; 17 | case 'UserNotConfirmedException': 18 | errorMessage = USER_NOT_VERIFIED; 19 | break; 20 | } 21 | 22 | // if error of type then send a 'CLEAN_AUTH' action 23 | const notAuthorized = (e.code === 'NotAuthorizedException'); 24 | if (notAuthorized) { 25 | yield put({type: 'CLEAN_AUTH'}); 26 | } 27 | 28 | // send an action of type which contains errorMessage 29 | yield put({ 30 | type: 'ATTEMPT_LOGIN_FAILURE', 31 | payload: { 32 | type: authScreen, 33 | errorMessage: errorMessage || e.message || 'Some error occured', 34 | }, 35 | }); 36 | } 37 | 38 | export default loginFailureSaga; 39 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/loginSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | put, 3 | select, 4 | call, 5 | } from 'redux-saga/effects'; 6 | import { Auth } from 'aws-amplify'; 7 | 8 | /** 9 | * Login Form Selector gets values stored in redux-store of the login form 10 | */ 11 | const loginFormSelector = state => state.form.login.values; 12 | 13 | /** 14 | * Returns the signed-in user, (AWS Cognito) 15 | * contains the Token ID eg., idToken, accessToken, refreshToken 16 | */ 17 | const currentAuthenticatedUserPromise = () => Auth.currentAuthenticatedUser(); 18 | 19 | /** 20 | * Returns the signed in user's other data, ( which were asked during sign-up ) 21 | * eg., Full Name, etc... 22 | */ 23 | const userDataPromise = () => Auth.currentUserInfo(); 24 | 25 | /** 26 | * Login Promise returned by Amplify's 'signIn' method 27 | * @param username {string} email of user 28 | * @param password {string} password 29 | * @returns {Promise} 30 | */ 31 | const loginPromise = ({ username, password }) => Auth.signIn(username, password); 32 | 33 | /** 34 | * Login Function to login the user 35 | * @param action 36 | */ 37 | function* loginSaga(username, password) { 38 | if (!username && !password) { 39 | const loginValues = yield select(loginFormSelector); 40 | username = loginValues.username; 41 | password = loginValues.password; 42 | } 43 | 44 | /** 45 | * Make an API request to sign in the user 46 | * with entered values 47 | */ 48 | yield call(loginPromise, { username, password }); 49 | 50 | /** 51 | * Get the credentials in case successful signIn 52 | * Contains the various token needed and other info 53 | */ 54 | const currentCredentials = yield call(currentAuthenticatedUserPromise); 55 | 56 | /** 57 | * Get user's other attributes entered during sign-up 58 | * eg., FullName, Phone Number 59 | */ 60 | const currentUserData = yield call(userDataPromise); 61 | 62 | /** 63 | * Send new action which saves all info 64 | * of the signed in user into the redux store 65 | */ 66 | yield put({ 67 | type: 'ATTEMPT_LOGIN_SUCCESS', 68 | payload: { 69 | identityId: currentCredentials, 70 | userData: currentUserData, 71 | }, 72 | }); 73 | } 74 | 75 | export default loginSaga; 76 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/registerSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | put, 3 | select, 4 | call, 5 | } from 'redux-saga/effects'; 6 | import { Auth } from 'aws-amplify'; 7 | 8 | /** 9 | * Register form values in redux store 10 | */ 11 | const registerFormSelector = state => state.form.register.values; 12 | 13 | /** 14 | * Sign-Up promise returned by AWS Amplify 'signUp' method 15 | * @param name {string} full name of user, asked during sign-up 16 | * @param username {string} email for user, unique for each user 17 | * @param password {string} user's password 18 | * @param phone {number} user's phoneNumber 19 | * @returns {Promise} 20 | */ 21 | const signUpPromise = ({name, username, password, phone}) => Auth.signUp({ 22 | username, 23 | password, 24 | attributes: { 25 | name, 26 | 'phone_number': phone, 27 | }, 28 | }); 29 | 30 | 31 | /** 32 | * Register the user 33 | */ 34 | function* registerSaga() { 35 | const { name, username, password, phone } = yield select(registerFormSelector); 36 | 37 | /** 38 | * Make an API call to AWS Cognito with the values in form 39 | */ 40 | yield call(signUpPromise, { 41 | name, 42 | username, 43 | password, 44 | phone 45 | }); 46 | 47 | /** 48 | * Send a new action so that user enters the verification code to confirm sign-up 49 | * Verification code is sent on email-id 50 | */ 51 | yield put({type: 'VERIFICATION_CODE'}); 52 | } 53 | 54 | export default registerSaga; 55 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/requestVerificationCodeSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeLatest, 3 | put, 4 | select, 5 | call, 6 | } from 'redux-saga/effects'; 7 | import { Auth } from 'aws-amplify'; 8 | import loginFailureSaga from './loginFailureSaga'; 9 | 10 | /** 11 | * Get login form field values 12 | */ 13 | const loginFormSelector = state => state.form.login.values; 14 | 15 | /** 16 | * Get register form field values 17 | */ 18 | const registerFormSelector = state => state.form.register.values; 19 | 20 | /** 21 | * When user's hasn't entered the verification code during sign-up 22 | * Call this promise once more to resend the code and verify 23 | */ 24 | const requestVC = ({ username }) => Auth.resendSignUp(username); 25 | 26 | /** 27 | * Call the saga to handle the action 28 | */ 29 | function* requestCode(action) { 30 | // Mark the process as in progress 31 | yield put({type: 'IN_PROGRESS'}); 32 | try { 33 | /** 34 | * if screen is register screen then get username value from that form 35 | * else get username value form login form 36 | */ 37 | const {username} = action.payload.authScreen === 'register'? yield select(registerFormSelector) : 38 | yield select(loginFormSelector) ; 39 | 40 | // Request to send the verification code once again for the username 41 | yield call(requestVC, { username }); 42 | yield put({ 43 | type: 'VERIFICATION_CODE', 44 | payload: {authScreen: action.payload.authScreen}, 45 | }); 46 | } catch (e) { 47 | console.log(e); 48 | /** 49 | * trigger saga in case any error occurs 50 | */ 51 | const loginFail = (() => (loginFailureSaga({e, authScreen: action.payload.authScreen }))); 52 | yield call(loginFail); 53 | } 54 | } 55 | 56 | /** 57 | * Saga function which takes the action REQUEST_VERIFICATION_CODE 58 | */ 59 | 60 | function* requestVerificationCodeSaga() { 61 | yield takeLatest('REQUEST_VERIFICATION_CODE', requestCode); 62 | } 63 | 64 | export default requestVerificationCodeSaga; 65 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/auth/verifyUserSaga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeLatest, 3 | put, 4 | select, 5 | call, 6 | } from 'redux-saga/effects'; 7 | import { Auth } from 'aws-amplify'; 8 | import loginFailureSaga from './loginFailureSaga'; 9 | import loginSaga from './loginSaga'; 10 | 11 | /** 12 | * Get field values of login form 13 | */ 14 | const loginFormSelector = state => state.form.login.values; 15 | 16 | /** 17 | * Get field values of register form 18 | */ 19 | const registerFormSelector = state => state.form.register.values; 20 | 21 | /** 22 | * Get value of verification code field 23 | */ 24 | const verificationFormSelector = state => state.form.verification.values; 25 | 26 | /** 27 | * Promise returned by confirmSignUp method 28 | * @param username {string} email 29 | * @param verification {number} code sent on email of user, required to confirm sign up 30 | */ 31 | const confirmSignUpPromise = ({ username, verification }) => Auth.confirmSignUp(username, verification); 32 | 33 | function* confirmCode(action) { 34 | yield put({ 35 | type: 'IN_PROGRESS', 36 | }); 37 | try { 38 | // get field values of username and password based on which screen the action was triggered from 39 | const {username, password} = action.payload.authScreen === 'register'? yield select(registerFormSelector) : 40 | yield select(loginFormSelector) ; 41 | 42 | // get verification code inpu value 43 | const {verification} = yield select(verificationFormSelector); 44 | 45 | // Confirm the sign up for username with the entered code 46 | yield call(confirmSignUpPromise, { username, verification }); 47 | 48 | // Sign the user in once the sign up is successful 49 | const login = () => loginSaga(username, password); 50 | yield call(login); 51 | } catch (e) { 52 | console.log(e); 53 | /** 54 | * Trigger the saga if any error occurs 55 | */ 56 | const loginFail = (() => (loginFailureSaga({e, authScreen: action.payload.authScreen }))); 57 | yield call(loginFail); 58 | } 59 | } 60 | 61 | // Saga triggered for the action type 'CONFIRM_VERIFICATION_CODE' 62 | function* verifyUserSaga() { 63 | yield takeLatest('CONFIRM_VERIFICATION_CODE', confirmCode); 64 | } 65 | 66 | export default verifyUserSaga; 67 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/cart/cartItemsAddSaga.js: -------------------------------------------------------------------------------- 1 | import { put, call, select, takeLatest } from 'redux-saga/effects'; 2 | import CartService from '../../service/cart'; 3 | import { deDupeItems } from '../../utils/array'; 4 | 5 | /** 6 | * Get userId value from redux store 7 | */ 8 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 9 | 10 | /** 11 | * Get current cart items from the store 12 | */ 13 | const cartItemsSelector = state => state.cart.cartData || []; 14 | 15 | const { updateCart } = CartService; 16 | 17 | /** 18 | * saga called on action 19 | */ 20 | function* cartItemsAdd(action) { 21 | try { 22 | // get userId from the store 23 | const userId = yield select(userIdSelector); 24 | 25 | // get current Cart items from store 26 | const currentCart = yield select(cartItemsSelector); 27 | 28 | // Create new empty cart 29 | let newCart = []; 30 | // merge the current cart with the newly added item 31 | // in case the newly added item is already in cart 32 | // then increase the quantity of that item 33 | newCart = deDupeItems([...currentCart, ...[action.payload]]); 34 | 35 | // Make API request to update cart at backend for that user 36 | const response = yield call(() => updateCart(userId, newCart)); 37 | const { resp } = response.data ? response.data : {}; 38 | 39 | // trigger the action which will fetch new cart data from backend 40 | yield put({ type: 'FETCH_CART_ITEMS' }); 41 | } catch (e) { 42 | console.log(e); 43 | } 44 | } 45 | 46 | /** 47 | * Saga which handles the action UPDATE_CART_ITEMS 48 | */ 49 | function* cartItemsAddSaga() { 50 | yield takeLatest('UPDATE_CART_ITEMS', cartItemsAdd); 51 | } 52 | 53 | export default cartItemsAddSaga; 54 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/cart/cartItemsCleanSaga.js: -------------------------------------------------------------------------------- 1 | import { put, call, select, takeLatest } from 'redux-saga/effects'; 2 | import CartService from '../../service/cart'; 3 | 4 | /** 5 | * Get userId value from redux store 6 | */ 7 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 8 | const { updateCart } = CartService; 9 | 10 | 11 | function* cartItemsClean(action) { 12 | try { 13 | /** 14 | * Get userId from redux store 15 | */ 16 | const userId = yield select(userIdSelector); 17 | 18 | // create a new empty cart 19 | const newCart = []; 20 | // send the empty cart to API and overwrite to an empty version 21 | const response = yield call(() => updateCart(userId, newCart)); 22 | 23 | // send action to refect cart items from backend 24 | // response returned would be an empty cart in this case 25 | yield put({ type: 'FETCH_CART_ITEMS' }); 26 | } catch (e) { 27 | console.log(e); 28 | } 29 | } 30 | 31 | /** 32 | * Saga to handle action CLEAN_CART_ITEMS 33 | */ 34 | function* cartItemsCleanSaga() { 35 | yield takeLatest('CLEAN_CART_ITEMS', cartItemsClean); 36 | } 37 | 38 | export default cartItemsCleanSaga; 39 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/cart/cartItemsDeleteSaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import CartService from '../../service/cart'; 3 | 4 | /** 5 | * Get userId value from redux store 6 | */ 7 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 8 | 9 | /** 10 | * Get Cart Items from redux store 11 | */ 12 | const cartItemsSelector = state => state.cart.cartData || []; 13 | const { updateCart, getCartDetails } = CartService; 14 | 15 | function* cartItemDelete(action) { 16 | try { 17 | // get userId from store 18 | const userId = yield select(userIdSelector); 19 | 20 | // get current cart values from the store 21 | const currentCart = yield select(cartItemsSelector); 22 | 23 | // create a new cart without the deleted cart item 24 | const newCart = currentCart.filter(obj => obj.groceryId !== action.payload); 25 | 26 | // send the new Cart version to backend to update the cart 27 | const response = yield call(() => updateCart(userId, newCart)); 28 | const { resp } = response.data ? response.data : {}; 29 | 30 | // get details of cart items which are present in cart 31 | // details include image url, price, category etc 32 | let cartDetails = yield call(() => getCartDetails(userId)); 33 | 34 | // if more than 1 cart Details present set to value else an empty array 35 | cartDetails = cartDetails.data.length > 0 ? cartDetails.data : []; 36 | 37 | // Save the new received cart from backend 38 | yield put({ type: 'SAVE_NEW_CART', payload: response.data.Attributes.cartData }); 39 | 40 | // Save the details of new cart items 41 | yield put({ type: 'SAVE_NEW_CART_INFO', payload: cartDetails || [] }); 42 | } catch (e) { 43 | console.log(e); 44 | } 45 | } 46 | 47 | /** 48 | * Saga to handle the action of deleting a cart item 49 | */ 50 | function* cartItemsDeleteSaga() { 51 | yield takeLatest('DELETE_CART_ITEM', cartItemDelete); 52 | } 53 | 54 | export default cartItemsDeleteSaga; 55 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/cart/cartItemsFetchSaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import CartService from '../../service/cart'; 3 | 4 | /** 5 | * Get userId value from redux store 6 | */ 7 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 8 | const { getCart, getCartDetails } = CartService; 9 | 10 | function* cartItemsFetch(action) { 11 | // action to inform cart fetch in progress 12 | yield put({ 13 | type: 'IN_PROGRESS', 14 | }); 15 | try { 16 | // get userId value from store 17 | const userId = yield select(userIdSelector); 18 | 19 | // get cart for the user with the specified userid 20 | const response = yield call(() => getCart(userId)); 21 | 22 | // get details of cart items 23 | let cartDetails = yield call(() => getCartDetails(userId)); 24 | cartDetails = cartDetails.data.length > 0 ? cartDetails.data : []; 25 | 26 | const { cartData } = response.data.Item ? response.data.Item : []; 27 | 28 | // save the cart data for the user 29 | yield put({ 30 | type: 'USER_CART_ITEMS', 31 | payload: { 32 | cartData, 33 | }, 34 | }); 35 | 36 | // save the cart items info 37 | yield put({ 38 | type: 'SAVE_ITEM_INFO', 39 | payload: cartDetails || [], 40 | }); 41 | } catch (e) { 42 | console.log(e); 43 | // in case of error set cart data as empty 44 | yield put({ 45 | type: 'USER_CART_ITEMS', 46 | payload: { 47 | cartData: [], 48 | }, 49 | }); 50 | 51 | yield put({ 52 | type: 'SAVE_ITEM_INFO', 53 | payload: [], 54 | }); 55 | } 56 | } 57 | 58 | function* cartItemsFetchSaga() { 59 | yield takeLatest('FETCH_CART_ITEMS', cartItemsFetch); 60 | } 61 | 62 | export default cartItemsFetchSaga; 63 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/cart/cartItemsUpdateQtySaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import CartService from '../../service/cart'; 3 | 4 | /** 5 | * Get userId value from redux store 6 | */ 7 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 8 | 9 | /** 10 | * Get cart items value from store 11 | */ 12 | const cartItemsSelector = state => state.cart.cartData; 13 | 14 | const { updateCart, getCartDetails } = CartService; 15 | 16 | function* cartItemUpdateQty(action) { 17 | try { 18 | // get userId from store 19 | const userId = yield select(userIdSelector); 20 | 21 | // get current cart value from store 22 | const currentCart = yield select(cartItemsSelector); 23 | 24 | // create new cart with updated item quantity 25 | const newCart = currentCart.map((obj) => { 26 | if (obj.groceryId === action.payload.groceryId) { 27 | const newObj = obj; 28 | newObj.qty = action.payload.qty; 29 | return newObj; 30 | } 31 | return obj; 32 | }); 33 | 34 | // send the updated cart to backend 35 | const response = yield call(() => updateCart(userId, newCart)); 36 | const { resp } = response.data ? response.data : {}; 37 | 38 | // get details of new cart items 39 | const cartDetails = yield call(() => getCartDetails(userId)); 40 | 41 | // send action to save cart items in store 42 | yield put({ type: 'SAVE_NEW_CART', payload: response.data.Attributes.cartData }); 43 | 44 | // send action to save cart items details in store 45 | yield put({ 46 | type: 'SAVE_ITEM_INFO', 47 | payload: cartDetails.data || [], 48 | }); 49 | } catch (e) { 50 | console.log(e); 51 | } 52 | } 53 | 54 | /** 55 | * Saga to handle cart item quantity change 56 | */ 57 | function* cartItemUpdateQtySaga() { 58 | yield takeLatest('UPDATE_CART_ITEM_QTY', cartItemUpdateQty); 59 | } 60 | 61 | export default cartItemUpdateQtySaga; 62 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import { fork } from 'redux-saga/effects'; 2 | 3 | import authenticationSaga from './auth/authenticationSaga'; 4 | import verifyUserSaga from './auth/verifyUserSaga'; 5 | import forgotPasswordRequestSaga from './auth/forgotPasswordRequestSaga'; 6 | import forgotPasswordSaga from './auth/forgotPasswordSaga'; 7 | import requestVerificationCodeSaga from './auth/requestVerificationCodeSaga'; 8 | import cartItemsFetchSaga from './cart/cartItemsFetchSaga'; 9 | import cartItemsAddSaga from './cart/cartItemsAddSaga'; 10 | import cartItemsDeleteSaga from './cart/cartItemsDeleteSaga'; 11 | import cartItemUpdateQtySaga from './cart/cartItemsUpdateQtySaga'; 12 | import cartItemsCleanSaga from './cart/cartItemsCleanSaga'; 13 | 14 | import placeOrderSaga from './order/placeOrderSaga'; 15 | import cleanOrderSaga from './order/cleanOrderSaga'; 16 | import fetchOrderSaga from './order/fetchAllOrdersSaga'; 17 | import cancelOrderSaga from './order/cancelOrderSaga'; 18 | 19 | import paymentTokenIdSubmitSaga from './payment/paymentTokenIdSubmitSaga'; 20 | 21 | function* rootSaga() { 22 | yield fork(authenticationSaga); 23 | yield fork(verifyUserSaga); 24 | yield fork(forgotPasswordRequestSaga); 25 | yield fork(forgotPasswordSaga); 26 | yield fork(cartItemsFetchSaga); 27 | yield fork(cartItemsAddSaga); 28 | yield fork(cartItemsDeleteSaga); 29 | yield fork(cartItemUpdateQtySaga); 30 | yield fork(cartItemsCleanSaga); 31 | yield fork(placeOrderSaga); 32 | yield fork(cleanOrderSaga); 33 | yield fork(paymentTokenIdSubmitSaga); 34 | yield fork(fetchOrderSaga); 35 | yield fork(cancelOrderSaga); 36 | yield fork(requestVerificationCodeSaga); 37 | } 38 | 39 | export default rootSaga; 40 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/order/cancelOrderSaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import OrderService from '../../service/order'; 3 | 4 | /** 5 | * Get userid from store 6 | */ 7 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 8 | 9 | /** 10 | * Get the 'first' order whose status is payment pending and return its ID 11 | * at a time only one order will be in pending status 12 | */ 13 | const orderIdSelector = (state) => { 14 | if (state.order && state.order.orderList) { 15 | const { orderList } = state.order; 16 | if (orderList.length > 0) { 17 | const idx = orderList.findIndex(order => order.orderStatus === 'PAYMENT_PENDING'); 18 | if (idx >= 0) { 19 | return (orderList[idx].orderId || -1); 20 | } 21 | return -1; 22 | } 23 | return -1; 24 | } 25 | return -1; 26 | }; 27 | 28 | 29 | const { cancelOrderAPI } = OrderService; 30 | 31 | function* cancelOrder(action) { 32 | try { 33 | // get userid from store 34 | const userId = yield select(userIdSelector); 35 | 36 | // get the order id 37 | const orderId = yield select(orderIdSelector); 38 | 39 | // Don't make any request if there's no pending order; 40 | if (orderId !== -1) { 41 | // send the order ID onto server to cancel the order at backend 42 | const response = yield call(() => cancelOrderAPI(userId, orderId)); 43 | 44 | const { resp } = response.data ? response.data : {}; 45 | 46 | // send an action to fetch an updated list of all order 47 | yield put({ 48 | type: 'FETCH_ALL_ORDERS', 49 | }); 50 | } 51 | } catch (e) { 52 | console.log(e); 53 | } 54 | } 55 | 56 | /** 57 | * Saga to handle the order cancel action 58 | */ 59 | function* cancelOrderSaga() { 60 | yield takeLatest('CANCEL_ORDER', cancelOrder); 61 | } 62 | 63 | export default cancelOrderSaga; 64 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/order/cleanOrderSaga.js: -------------------------------------------------------------------------------- 1 | import { put, takeLatest } from 'redux-saga/effects'; 2 | 3 | function* cleanOrderAndPayment(action) { 4 | // remove values in order reducer 5 | yield put({ type: 'CLEAR_ORDER' }); 6 | // remove values in payment reducer 7 | yield put({ type: 'CLEAR_PAYMENT' }); 8 | } 9 | 10 | /** 11 | * Saga to clear everything in order reducer and payment reducer 12 | */ 13 | function* cleanOrderSaga() { 14 | yield takeLatest('CLEAN_ORDER', cleanOrderAndPayment); 15 | } 16 | 17 | export default cleanOrderSaga; 18 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/order/fetchAllOrdersSaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import OrderService from '../../service/order'; 3 | 4 | // get userId from store 5 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 6 | const { fetchOrderAPI } = OrderService; 7 | 8 | function* fetchOrder(action) { 9 | try { 10 | const userId = yield select(userIdSelector); 11 | 12 | // get all order for the userId 13 | const response = yield call(() => fetchOrderAPI(userId)); 14 | 15 | const { resp } = response.data ? response.data : {}; 16 | 17 | let pendingOrder = null; 18 | 19 | // if orders present 20 | if (response.data.length > 0) { 21 | // get the order which is in pending state and save in 'currentOrder' in order store 22 | const idx = response.data.findIndex(order => order.orderStatus === 'PAYMENT_PENDING'); 23 | pendingOrder = response.data[idx]; 24 | } 25 | 26 | // send action to save info in store 27 | yield put({ 28 | type: 'SAVE_ALL_ORDERS', 29 | payload: response.data, 30 | pendingOrder: pendingOrder || null, 31 | }); 32 | } catch (e) { 33 | console.log(e); 34 | } 35 | } 36 | 37 | /** 38 | * Saga to handle all order fetching 39 | */ 40 | function* fetchOrderSaga() { 41 | yield takeLatest('FETCH_ALL_ORDERS', fetchOrder); 42 | } 43 | 44 | export default fetchOrderSaga; 45 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/order/placeOrderSaga.js: -------------------------------------------------------------------------------- 1 | import { put, call, select, takeLatest } from 'redux-saga/effects'; 2 | import OrderService from '../../service/order'; 3 | 4 | // get userId from store 5 | const userIdSelector = state => state.auth.userData && state.auth.userData.username; 6 | const { placeOrderAPI } = OrderService; 7 | 8 | function* placeOrder(action) { 9 | try { 10 | const userId = yield select(userIdSelector); 11 | 12 | // make request to backend to place an order 13 | // order will be created for all items present in cart 14 | // reponse contains order Id and total amount 15 | const response = yield call(() => placeOrderAPI(userId)); 16 | 17 | const { resp } = response.data ? response.data : {}; 18 | 19 | // Save newly placed Order info 20 | yield put({ 21 | type: 'SAVE_ORDER_ID', 22 | payload: response.data, 23 | }); 24 | 25 | // send action to refetch all order 26 | yield put({ 27 | type: 'FETCH_ALL_ORDERS', 28 | }); 29 | } catch (e) { 30 | console.log(e); 31 | } 32 | } 33 | 34 | /** 35 | * Saga to handle order placing 36 | */ 37 | function* placeOrderSaga() { 38 | yield takeLatest('PLACE_ORDER', placeOrder); 39 | } 40 | 41 | export default placeOrderSaga; 42 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/sagas/payment/paymentTokenIdSubmitSaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from 'redux-saga/effects'; 2 | import PaymentRequests from '../../service/payment'; 3 | 4 | const { submitPaymentRequest } = PaymentRequests; 5 | 6 | function* submitPayment(action) { 7 | const { 8 | tokenId, orderId, email, userId, 9 | } = action.payload; 10 | if (!tokenId || !orderId) return; 11 | const payload = { 12 | email, 13 | stripeId: tokenId, 14 | orderId, 15 | userId, 16 | }; 17 | yield put({ 18 | type: 'PAYMENT_IN_PROGRESS', 19 | }); 20 | try { 21 | // make request to process payment with the passed info 22 | // server will confirm order only if payment is successful 23 | const response = yield call(() => submitPaymentRequest(payload)); 24 | const { data } = response; 25 | if (data.success) { 26 | yield put({ 27 | type: 'PAYMENT_SUCCESS', 28 | }); 29 | yield put({ 30 | type: 'FETCH_ALL_ORDERS', 31 | }); 32 | } else { 33 | yield put({ 34 | type: 'PAYMENT_FAILURE', 35 | payload: { error: data.error }, 36 | }); 37 | } 38 | } catch (e) { 39 | yield put({ 40 | type: 'PAYMENT_FAILURE', 41 | payload: { error: 'Something went wrong, Please try again.' }, 42 | }); 43 | } 44 | } 45 | 46 | /** 47 | * Saga to handle payment Token id submit on server 48 | */ 49 | function* paymentTokenIdSubmitSaga() { 50 | yield takeLatest('SUBMIT_PAYMENT_TOKEN_ID', submitPayment); 51 | } 52 | 53 | export default paymentTokenIdSubmitSaga; 54 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/bill-receipt.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | import {cartItemsInfoSelector} from './common/cart-items-info'; 4 | import {currentOrderSelector} from './common/current-order'; 5 | import {userDataSelector} from './common/user-data'; 6 | 7 | export const billReceiptSelector = createSelector( 8 | [ 9 | cartItemsInfoSelector, 10 | currentOrderSelector, 11 | (state) => state.payment.paymentComplete, 12 | (state) => {return (state.payment.paymentInProgress || false);}, 13 | userDataSelector 14 | ], ({cartItemsInfo}, {isCurrentOrderEmpty, orderId, orderTotal}, 15 | paymentComplete, paymentInProgress, {username}) => { 16 | return ({ 17 | isCurrentOrderEmpty, 18 | orderId, 19 | orderTotal, 20 | cartItems: cartItemsInfo, 21 | paymentInProgress, 22 | paymentComplete, 23 | username 24 | }); 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/cart-home.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import {cartItemsInfoSelector} from './common/cart-items-info'; 3 | import {currentOrderSelector} from './common/current-order'; 4 | import {userDataSelector} from './common/user-data'; 5 | import {cartDataSelector} from './common/cart-data'; 6 | 7 | export const cartHomeSelector = createSelector( 8 | [ 9 | cartDataSelector, 10 | cartItemsInfoSelector, 11 | currentOrderSelector, 12 | userDataSelector, 13 | (state) => state.payment.paymentComplete, 14 | (state) => {return (state.payment.paymentInProgress || false);}, 15 | (state) => state.cart.inProgress 16 | ], ({isCartDataEmpty, cartData}, {cartItemsInfo, isCartItemsInfoEmpty, totalBill}, 17 | {isCurrentOrderEmpty, orderId, orderStatus, orderTotal}, {username}, 18 | paymentComplete, paymentInProgress, inProgress) => { 19 | return ({ 20 | isCurrentOrderEmpty, 21 | orderTotal, 22 | orderStatus, 23 | orderId, 24 | paymentInProgress, 25 | username, 26 | isCartItemsInfoEmpty, 27 | cartItemsInfo, 28 | cartItems: cartData, 29 | totalBill, 30 | isCartItemsEmpty: isCartDataEmpty, 31 | paymentComplete, 32 | fetchingCart: inProgress 33 | }); 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/common/cart-data.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | export const cartDataSelector = createSelector( 5 | [ 6 | (state) => {return (state.cart.cartData || []);}, 7 | ], (cartData) => { 8 | const isCartDataEmpty = isEmpty(cartData); 9 | const cartDataLength = cartData.length; 10 | return ({ 11 | isCartDataEmpty, 12 | cartDataLength, 13 | cartData 14 | }); 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/common/cart-items-info.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | export const cartItemsInfoSelector = createSelector( 5 | [ 6 | (state) => {return (state.cart.cartItemsInfo || []);}, 7 | ], (cartItemsInfo) => { 8 | const isCartItemsInfoEmpty = isEmpty(cartItemsInfo); 9 | const totalBill = cartItemsInfo.reduce((total, cur) => total += cur.price * cur.qty, 0); 10 | return ({ 11 | cartItemsInfo, 12 | isCartItemsInfoEmpty, 13 | totalBill, 14 | }); 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/common/current-order.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | export const currentOrderSelector = createSelector( 5 | [ 6 | (state) => {return (state.order.currentOrder || {});}, 7 | ], (currentOrder) => { 8 | const isCurrentOrderEmpty = isEmpty(currentOrder); 9 | const {orderId, orderTotal, orderStatus} = currentOrder; 10 | return ({ 11 | isCurrentOrderEmpty, 12 | orderId, 13 | orderTotal, 14 | orderStatus 15 | }); 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/common/user-data.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | export const userDataSelector = createSelector( 4 | [ 5 | (state) => state.auth.userData, 6 | ], (userData) => { 7 | const {username, attributes} = userData; 8 | return ({ 9 | username, 10 | attributes 11 | }); 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/header.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import {cartDataSelector} from './common/cart-data'; 3 | 4 | export const headerSelector = createSelector( 5 | [ 6 | cartDataSelector, 7 | (state) => state.order.orderListFetched, 8 | ], ({isCartDataEmpty, cartDataLength}, orderListFetched) => { 9 | return {orderListFetched, cartDataLength, isCartDataEmpty}; 10 | } 11 | ) 12 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/order-list.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import {userDataSelector} from './common/user-data'; 3 | 4 | import isEmpty from 'lodash/isEmpty'; 5 | import sortBy from 'lodash/sortBy'; 6 | 7 | export const orderListSelector = createSelector( 8 | [ 9 | (state) => state.order.orderList, 10 | (state) => state.order.orderListFetched, 11 | (state) => {return (state.order.currentOrder || {});}, 12 | userDataSelector, 13 | (state) => state.payment.paymentInProgress, 14 | (state) => state.payment.paymentComplete, 15 | ], (orderList, orderListFetched, currentOrder, {username}, paymentInProgress, paymentComplete) => { 16 | const isOrderlistEmpty = isEmpty(orderList); 17 | const sortedOrderList = isOrderlistEmpty? [] : sortBy(orderList, (item) => { 18 | const timeStamp = new Date(item.orderDate); 19 | const inMillisec = timeStamp.getTime(); 20 | return -inMillisec; 21 | }); 22 | const {orderTotal, orderId} = currentOrder; 23 | return ({ 24 | orderList: sortedOrderList, 25 | isOrderlistEmpty, 26 | orderListFetched, 27 | orderTotal, 28 | orderId, 29 | username, 30 | paymentInProgress, 31 | paymentComplete 32 | }); 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/selectors/profile-home.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import {userDataSelector} from './common/user-data'; 3 | import isEmpty from 'lodash/isEmpty'; 4 | 5 | export const profileHomeSelector = createSelector( 6 | [ 7 | userDataSelector 8 | ], ({attributes}) => { 9 | const {phone_number, name, email, email_verified, phone_number_verified} = attributes; 10 | const isPhoneNumberEmpty = isEmpty(phone_number); 11 | const isFullNameEmpty = isEmpty(name); 12 | return { 13 | isPhoneNumberEmpty, 14 | isFullNameEmpty, 15 | phoneNumber: phone_number, 16 | name, 17 | email, 18 | emailVerified: email_verified, 19 | phoneNumberVerified: phone_number_verified 20 | }; 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/service/api_constants.js: -------------------------------------------------------------------------------- 1 | export const API_BASE = 'http://localhost:3000'; 2 | 3 | export const GROCERY_INFO_URL = '/grocery'; 4 | export const GROCERIES_URL = '/groceries'; 5 | export const CATEGORY_URL = `${GROCERIES_URL}?category=`; 6 | export const CART_URL = '/cart'; 7 | export const CART_DETAILS_URL = '/cartDetails'; 8 | export const PAYMENT_URL = '/pay'; 9 | export const ORDER_URL = '/createOrder'; 10 | export const ORDER_PENDING_URL = '/getOrders'; 11 | export const ORDER_CANCEL_URL = '/cancelOrder'; 12 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/service/cart.js: -------------------------------------------------------------------------------- 1 | import { CART_DETAILS_URL, CART_URL } from './api_constants'; 2 | import request from './request'; 3 | 4 | /** 5 | * Get current Cart for a user 6 | * @param userId {string} pass the userId for whom data is to be fetched 7 | */ 8 | function getCart(userId) { 9 | const queryString = `?userId=${userId}`; 10 | return request({ url: `${CART_URL}${queryString}`, method: 'GET' }); 11 | } 12 | 13 | /** 14 | * Update Cart Items 15 | * @param userId {string} pass the userId for whom data is to be updated 16 | * @param data {array} array of items which will be added in cart 17 | */ 18 | function updateCart(userId, data) { 19 | const postData = { 20 | userId, 21 | cartData: data, 22 | }; 23 | return request({ url: CART_URL, method: 'POST', data: postData }); 24 | } 25 | 26 | /** 27 | * Get cart for the user 28 | * @param userId 29 | */ 30 | function getCartDetails(userId) { 31 | const queryString = `?userId=${userId}`; 32 | return request({ url: `${CART_DETAILS_URL}${queryString}`, method: 'GET' }); 33 | } 34 | 35 | export default { 36 | getCart, 37 | getCartDetails, 38 | updateCart, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/service/grocery.js: -------------------------------------------------------------------------------- 1 | import { CATEGORY_URL, GROCERIES_URL, GROCERY_INFO_URL } from './api_constants'; 2 | import request from './request'; 3 | 4 | // Get top 3g groceries item for each category 5 | // Shown on home page 6 | export function getTop3Groceries() { 7 | return request({ url: GROCERIES_URL, method: 'GET' }); 8 | } 9 | 10 | // Get info about particular grocery item based on id 11 | export function getGroceryInfo(groceryId) { 12 | return request({ url: `${GROCERY_INFO_URL}?id=${groceryId}`, method: 'GET' }); 13 | } 14 | 15 | // Get groceries for a particular category 16 | export function getCategoryGroceries(category) { 17 | return request({ url: `${CATEGORY_URL + category}`, method: 'GET' }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/service/order.js: -------------------------------------------------------------------------------- 1 | import { ORDER_CANCEL_URL, ORDER_PENDING_URL, ORDER_URL } from './api_constants'; 2 | import request from './request'; 3 | 4 | /** 5 | * API for placing order for the userId 6 | * @param userId 7 | */ 8 | function placeOrderAPI(userId) { 9 | const data = { 10 | userId, 11 | }; 12 | return request({ url: ORDER_URL, method: 'POST', data }); 13 | } 14 | 15 | /** 16 | * Fetch all order present for the userID 17 | * @param userId 18 | */ 19 | function fetchOrderAPI(userId) { 20 | return request({ url: `${ORDER_PENDING_URL}?userId=${userId}`, method: 'GET' }); 21 | } 22 | 23 | 24 | /** 25 | * Cancel order with particular order Id 26 | * @param userId 27 | * @param orderId 28 | */ 29 | function cancelOrderAPI(userId, orderId) { 30 | const data = { 31 | userId, 32 | orderId, 33 | }; 34 | return request({ url: ORDER_CANCEL_URL, method: 'POST', data }); 35 | } 36 | 37 | export default { 38 | placeOrderAPI, 39 | fetchOrderAPI, 40 | cancelOrderAPI, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/service/payment.js: -------------------------------------------------------------------------------- 1 | import { PAYMENT_URL } from './api_constants'; 2 | import request from './request'; 3 | 4 | /** 5 | * Submit tokenId for the order payment 6 | * @param userId {string} pass the userId for whom data is to be fetched 7 | * @return {AxiosPromise} 8 | */ 9 | function submitPaymentRequest(data) { 10 | return request({ url: PAYMENT_URL, method: 'POST', data }); 11 | } 12 | 13 | export default { 14 | submitPaymentRequest, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/service/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_BASE } from './api_constants'; 3 | 4 | const client = axios.create({ 5 | baseURL: API_BASE, 6 | }); 7 | 8 | /** 9 | * Request Wrapper with default success/error actions 10 | */ 11 | const request = function (options) { 12 | const onSuccess = function (response) { 13 | console.debug('Request Successful!', response); 14 | return response; 15 | }; 16 | 17 | const onError = function (error) { 18 | console.error('Request Failed:', error.config); 19 | if (error.response) { 20 | console.error('Status:', error.response.status); 21 | console.error('Data:', error.response.data); 22 | console.error('Headers:', error.response.headers); 23 | } else { 24 | console.error('Error Message:', error.message); 25 | } 26 | return Promise.reject(error.response || error.message); 27 | }; 28 | 29 | return client(options) 30 | .then(onSuccess) 31 | .catch(onError); 32 | }; 33 | 34 | export default request; 35 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import { createLogger } from 'redux-logger'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | import { composeWithDevTools } from 'redux-devtools-extension'; 5 | import { applyMiddleware, combineReducers, createStore } from 'redux'; 6 | 7 | import authReducer from './Auth/authReducer'; 8 | import cart from './reducers/cart'; 9 | import rootSaga from './sagas'; 10 | import payment from './reducers/payment'; 11 | import order from './reducers/orders'; 12 | 13 | const logger = createLogger({}); 14 | const sagaMiddleware = createSagaMiddleware(); 15 | 16 | const rootReducer = combineReducers({ 17 | auth: authReducer, 18 | form: formReducer, 19 | cart, 20 | order, 21 | payment, 22 | }); 23 | 24 | const store = createStore( 25 | rootReducer, 26 | composeWithDevTools(applyMiddleware(sagaMiddleware, logger)), 27 | ); 28 | 29 | sagaMiddleware.run(rootSaga); 30 | 31 | export default store; 32 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/utils/array.js: -------------------------------------------------------------------------------- 1 | /** 2 | Merge the two different sources of same cart items. 3 | */ 4 | 5 | export const deDupeItems = Items => Items.reduce((total, cur) => { 6 | const i = total.findIndex(obj => obj.groceryId === cur.groceryId); 7 | if (i >= 0) { 8 | total[i].qty += cur.qty; 9 | } else { 10 | total.push(cur); 11 | } 12 | return total; 13 | }, []); 14 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/utils/string.js: -------------------------------------------------------------------------------- 1 | 2 | // Add new method to String class to convert string into ProperCase 3 | // eg., a title => A Title 4 | 5 | export function toProperCase(st) { 6 | return st.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()); 7 | } 8 | 9 | export default { 10 | toProperCase, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/CB-serverless-frontend/src/utils/stripe-payment-modal.js: -------------------------------------------------------------------------------- 1 | const publishKey = 'pk_test_rM2enW1rNROwx4ukBXGaIzhr'; 2 | 3 | /** 4 | Open Stripe payment modal for the payment 5 | */ 6 | 7 | export const displayPaymentModal = (props, onOpened, onClosed, onSubmit) => { 8 | const checkoutHandler = window.StripeCheckout.configure({ 9 | key: publishKey, 10 | locale: 'auto', 11 | }); 12 | checkoutHandler.open({ 13 | name: `Pay Rs.${props.orderTotal}`, 14 | description: `Order: ${props.orderId}`, 15 | closed: () => { 16 | onClosed && onClosed(); 17 | }, 18 | opened: () => { 19 | onOpened && onOpened(); 20 | }, 21 | token: (token) => { 22 | if (token && token.id) { 23 | onSubmit && onSubmit({ 24 | tokenId: token.id, 25 | orderId: props.orderId, 26 | email: token.email, 27 | userId: props.username, 28 | }); 29 | } else { 30 | // to do 31 | } 32 | }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /scripts/backendScripts.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const _ = require('lodash'); 4 | 5 | const generateModel = (fileName, config) => { 6 | var modelItems = ''; 7 | for (var i = 0; i < config.length; i++) { 8 | modelItems += config[i].type === 'date' ? `\t${config[i].props.name}: Date,\n` : `\t${config[i].props.name}: String,\n` 9 | } 10 | const result = `const mongoose = require('mongoose');\n\n` + 11 | `const ${_.capitalize(fileName)}Schema = new mongoose.Schema({\n` + 12 | modelItems + 13 | `});\n` + 14 | `\nexport default mongoose.model('${_.capitalize(fileName)}', ${_.capitalize(fileName)}Schema);` 15 | 16 | return result; 17 | }; 18 | 19 | generateApi = (fileName) => { 20 | const result = `import mongoose from 'mongoose';\n` + 21 | `import connectToDatabase from '../../db';\n` + 22 | `import ${_.capitalize(fileName)} from '../../models/${_.capitalize(fileName)}';\n\n` + 23 | 24 | `const renderServerError = (response, errorMessage) => response(null, {\n` + 25 | `\tstatusCode: 500,\n` + 26 | `\theaders: { 'Content-Type': 'application/json' },\n` + 27 | `\tbody: { success: false, error: errorMessage },\n` + 28 | `});\n\n` + 29 | `export const getAll${_.capitalize(fileName)} = (event, context, callback) => {\n` + 30 | `\tcontext.callbackWaitsForEmptyEventLoop = false;\n` + 31 | 32 | `\tconnectToDatabase().then(() => {\n` + 33 | `\t\t${_.capitalize(fileName)}.find({}, (error, data) => {\n` + 34 | `\t\t\tcallback(null, { statusCode: 200, headers: { 'Content-Type' : 'application/json' }, body: JSON.stringify(data) })\n` + 35 | `\t\t});\n` + 36 | `\t})\n` + 37 | `\t.catch(() => renderServerError(callback, 'Unable to fetch! Try again later'));\n` + 38 | `}\n\n` + 39 | `export const create${_.capitalize(fileName)} = (event, context, callback) => {\n` + 40 | `\tcontext.callbackWaitsForEmptyEventLoop = false;\n` + 41 | `\tconnectToDatabase().then(() => {\n` + 42 | `\t\t${_.capitalize(fileName)}.create(JSON.parse(event.body), (error, data) => {\n` + 43 | `\t\t\tcallback(null, { statusCode: 200, headers: { 'Content-Type' : 'application/json' }, body: JSON.stringify(data) })\n` + 44 | `\t\t});\n` + 45 | `\t})\n` + 46 | `\t.catch(() => renderServerError(callback, 'Unable to create! Try again later'));\n` + 47 | `}`; 48 | 49 | return result; 50 | } 51 | 52 | const insertNewHandlers = (handlerFile, fileName) => { 53 | 54 | fs.readFile(handlerFile, function(err, data) { 55 | if(err) throw err; 56 | //data = data.toString(); 57 | const importFileContent = `import { getAll${_.capitalize(fileName)}, create${_.capitalize(fileName)} } from './api/${fileName}';`; 58 | 59 | var array = [importFileContent, ...data.toString().split("\n")]; 60 | 61 | var insertToIndex; 62 | 63 | for (var i = 0; i < array.length; i++) { 64 | const foundIndex = _.includes(array[i], 'export {'); 65 | if (foundIndex) { 66 | insertToIndex = i; 67 | } 68 | } 69 | 70 | const insertContent = `\tgetAll${_.capitalize(fileName)},\n` + 71 | `\tcreate${_.capitalize(fileName)},` 72 | 73 | const newArray = [...array.slice(0, insertToIndex + 1), insertContent, ...array.slice(insertToIndex + 1)]; 74 | 75 | let result = ''; 76 | for(var u=0; u { 85 | const appendedYml = ( 86 | `\n create:` + 87 | `\n handler: handler.create${_.capitalize(fileName)}` + 88 | `\n events:` + 89 | `\n - http:` + 90 | `\n path: ${fileName}` + 91 | `\n method: post` + 92 | `\n cors: true\n` + 93 | `\n get:` + 94 | `\n handler: handler.getAll${_.capitalize(fileName)}` + 95 | `\n events:` + 96 | `\n - http:` + 97 | `\n path: ${fileName}` + 98 | `\n method: get` + 99 | `\n cors: true` 100 | ); 101 | fs.appendFileSync(handlerFile, appendedYml); 102 | } 103 | 104 | const generateBackEndFiles = (fileName, config) => { 105 | 106 | // Generate a Model 107 | const apiFolder = path.resolve(`./packages/CB-serverless-backend/api/${fileName}`); 108 | const modelFile = path.resolve(`./packages/CB-serverless-backend/models/${_.capitalize(fileName)}.js`); 109 | 110 | if (!fs.existsSync(apiFolder)) { 111 | fs.mkdirSync(apiFolder); 112 | } 113 | 114 | fs.writeFileSync(modelFile, generateModel(fileName, config)); 115 | 116 | // Create a folder under API. Create getFormData and postFormData 117 | const apiIndexFile = path.resolve(`./packages/CB-serverless-backend/api/${fileName}/index.js`); 118 | 119 | fs.writeFileSync(apiIndexFile, generateApi(fileName, config)); 120 | 121 | // Handlers import and export 122 | const handlerFile = path.resolve(`./packages/CB-serverless-backend/handler.js`); 123 | insertNewHandlers(handlerFile, fileName); 124 | 125 | // Serverless.yaml 126 | const serverlessYaml = path.resolve(`./packages/CB-serverless-backend/serverless.yml`); 127 | editYmlFile(serverlessYaml, fileName) 128 | 129 | } 130 | 131 | module.exports = { 132 | generateBackEndFiles: generateBackEndFiles, 133 | } -------------------------------------------------------------------------------- /scripts/htmlInputs.js: -------------------------------------------------------------------------------- 1 | const InputText = (design = "html", allProps) => { 2 | return ( 3 | `\t\n` + 4 | `\t\t\t\t
\n` + 5 | `\t\t\t\t\t` + 9 | `\n\t\t\t\t
` 10 | ) 11 | } 12 | 13 | const InputSelect = (design = "html", allProps, options) => { 14 | var menuItem = ''; 15 | for (var k = 0; k < options.length; k++) { 16 | menuItem += `\n\t\t\t\t\t\t`; 17 | } 18 | return ( 19 | `\t\n` + 20 | `\t\t\t\t
\n` + 21 | `\t\t\t\t\t` + 25 | `${menuItem}\n` + 26 | `\t\t\t\t\t` + 27 | `\n\t\t\t\t
` 28 | ); 29 | } 30 | 31 | const InputCheck = (design = "html", allProps) => { 32 | return ( 33 | `\t\n` + 34 | `\t\t\t\t
\n` + 35 | `\t\t\t\t\t` + 39 | `\n\t\t\t\t
` 40 | ) 41 | } 42 | 43 | const InputToggle = (design = "html", allProps) => { 44 | return ( 45 | `\t\n` + 46 | `\t\t\t\t
\n` + 47 | `\t\t\t\t\t` + 51 | `\n\t\t\t\t
` 52 | ) 53 | } 54 | 55 | const InputDatePicker = (design = "html", allProps) => { 56 | return ( 57 | `\t\n` + 58 | `\t\t\t\t
\n` + 59 | `\t\t\t\t\t` + 63 | `\n\t\t\t\t
` 64 | ) 65 | } 66 | 67 | module.exports = { 68 | InputText: InputText, 69 | InputCheck: InputCheck, 70 | InputSelect: InputSelect, 71 | InputToggle: InputToggle, 72 | InputDatePicker: InputDatePicker, 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /scripts/scaffold-form.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs-extra'); 3 | var util = require('util'); 4 | const _ = require('lodash'); 5 | const { generateBackEndFiles } = require('./backendScripts'); 6 | 7 | var { 8 | InputText, 9 | InputSelect, 10 | InputCheck, 11 | InputToggle, 12 | InputDatePicker, 13 | } = require('./htmlInputs'); 14 | 15 | // Function which logs any value 16 | function log(value) { 17 | console.log(util.inspect(value, false, null)); 18 | } 19 | 20 | var component, configFile; 21 | 22 | // Handles the command line 23 | var program = require('commander') 24 | .arguments('-n, --componentName', '') 25 | .arguments('-c', '--config', '') 26 | .action(function (componentName, formConfigFile) { 27 | component = componentName; 28 | configFile = formConfigFile; 29 | }) 30 | .parse(process.argv) 31 | 32 | // Scaffolds the form 33 | scaffoldForm(component, configFile); 34 | 35 | function generateInputBasedOnType(type, allProps, options) { 36 | switch (type) { 37 | case 'text': 38 | return InputText(type, allProps); 39 | case 'select': 40 | return InputSelect(type, allProps, options); 41 | case 'check': 42 | return InputCheck(type, allProps); 43 | case 'toggle': 44 | return InputToggle(type, allProps); 45 | case 'date': 46 | return InputDatePicker(type, allProps); 47 | } 48 | 49 | } 50 | 51 | function createEachFormElement(type, props, options) { 52 | // Get the props in array format 53 | const propsContent = Object.entries(props); 54 | var renderAllProps = ''; 55 | 56 | // Add up all props 57 | for (var j = 0; j < propsContent.length; j++) { 58 | renderAllProps += `\n\t\t\t\t\t\t${propsContent[j][0]}="${propsContent[j][1]}"`; 59 | } 60 | 61 | // Return the form Element with props 62 | return generateInputBasedOnType(type, renderAllProps, options); 63 | 64 | 65 | } 66 | 67 | // Renders the content of the form file 68 | function renderContent(fileName, formElements) { 69 | 70 | const imports = 'import {\n' + 71 | 'Checkbox,\n' + 72 | 'RadioButtonGroup,\n' + 73 | 'SelectField,\n' + 74 | 'TextField,\n' + 75 | 'Toggle,\n' + 76 | 'DatePicker\n' + 77 | `} from 'redux-form-material-ui';\n` + 78 | `import MenuItem from 'material-ui/MenuItem';\n` + 79 | `import { reduxForm, Field } from 'redux-form';\n`; 80 | 81 | return ( 82 | `/*\n Component generated: ${fileName} \n*/\n` + 83 | `import React, { Component } from 'react'\n` + 84 | imports + 85 | `\nclass ${_.capitalize(fileName)} extends Component {\n\n` + 86 | `\tcomponentDidMount() {\n` + 87 | `\t}\n\n` + 88 | `\trender() {\n` + 89 | `\t\treturn (` + 90 | `\n\t\t\t
` + 91 | `${formElements}\n` + 92 | `\t\t\t\t\n` + 93 | '\t\t\t
\n' + 94 | `\t\t);\n\n` + 95 | `\t}\n\n` + 96 | `}\n\n` + 97 | `export default reduxForm({` + 98 | `\n\tform: '${fileName}'` + 99 | `\n})(${_.capitalize(fileName)})` 100 | ) 101 | 102 | } 103 | 104 | function rootIndexContent(fileName) { 105 | return ( 106 | `import React from 'react';\n` + 107 | 108 | `import FormComponent from './${fileName}';\n\n` + 109 | `import config from '../../config';\n` + 110 | `import { API } from 'aws-amplify';\n\n` + 111 | 112 | `const ${_.capitalize(fileName)} = () => {\n` + 113 | `const handleSubmit = async (values) => {\n` + 114 | ` API.post('todo', '/${fileName}', {\n` + 115 | ` body: values,\n` + 116 | ` })\n` + 117 | `};\n` + 118 | 119 | '\treturn (\n' + 120 | '\t\t\n' + 123 | '\t);\n' + 124 | '}\n' + 125 | `export default ${_.capitalize(fileName)};` 126 | ); 127 | } 128 | 129 | function scaffoldForm(fileName, configName) { 130 | /* Add Front end related scaffolds */ 131 | 132 | // Component root folder 133 | const componentRootFolder = `./packages/CB-serverless-frontend/src/Forms/${fileName}`; 134 | const configRootFolder = `./configs/${configName}`; 135 | 136 | var root = path.resolve(componentRootFolder); 137 | var config = path.resolve(configRootFolder); 138 | 139 | var formConfig = require(config); 140 | 141 | var formElements = ''; 142 | const formElementValues = formConfig.formConfig.form; 143 | for (i = 0; i < formElementValues.length; i++) { 144 | const formConfigValues = formElementValues[i]; 145 | const { 146 | type, 147 | props, 148 | options = [] 149 | } = formConfigValues; 150 | formElements += createEachFormElement(type, props, options); 151 | } 152 | 153 | // Creates root folder 154 | if (!fs.existsSync(root)) { 155 | fs.mkdirSync(root); 156 | } 157 | 158 | // create a css file with the name provided for the scaffold 159 | fs.writeFileSync( 160 | path.join(root, `${fileName}.scss`), 161 | `/* CSS File for the ${fileName} form generated. You can add styles here to change the styling of this form*/\n` 162 | ) 163 | 164 | // Writes the content for JSX file which is the form 165 | fs.writeFileSync( 166 | path.join(root, `index.js`), 167 | rootIndexContent(fileName) 168 | ) 169 | // Writes the content for JSX file which is the form 170 | fs.writeFileSync( 171 | path.join(root, `${fileName}.js`), 172 | renderContent(fileName, formElements) 173 | ) 174 | var Routes = path.resolve('packages/CB-serverless-frontend/src/routes.js'); 175 | 176 | fs.readFile(Routes, function(err, data) { 177 | if(err) throw err; 178 | //data = data.toString(); 179 | const importFileContent = `import ${_.capitalize(fileName)} from './Forms/${fileName}';\n`; 180 | var array = [importFileContent, ...data.toString().split("\n")]; 181 | 182 | var insertToIndex; 183 | 184 | for (var i = 0; i < array.length; i++) { 185 | const foundIndex = _.includes(array[i], ''); 186 | if (foundIndex) { 187 | insertToIndex = i; 188 | } 189 | } 190 | 191 | const insertContent = ` \n`; 192 | 193 | const newArray = [...array.slice(0, insertToIndex + 1), insertContent, ...array.slice(insertToIndex + 1)]; 194 | 195 | let result = ''; 196 | for(var u=0; u