├── .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 | 
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 |
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 |
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 |
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 |
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 |
70 |
71 |
₹{` ${orderTotal}`}
72 |
73 |
74 | )
75 |
76 | renderListItem = ({index, name, qty, price}) => (
77 |
78 |
79 | - {name}
80 | - {`x ${qty}`}
81 |
82 |
85 |
86 | )
87 |
88 | render() {
89 | const {openDialog, closeDialog, openStripePaymentModal, paymentInProgress, order} = this.props;
90 | const {orderItems, orderTotal, orderId} = order;
91 | return (
92 |
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 |
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 |
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\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