}
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-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/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/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-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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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-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/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-frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/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/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/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-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/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-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-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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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/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-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-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-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-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-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-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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 {
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/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 |
--------------------------------------------------------------------------------