├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.json
├── client-dist
├── css
│ ├── app.084652a9.css
│ └── chunk-vendors.90d93e1b.css
├── favicon-32x32.png
├── img
│ ├── 1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg
│ ├── 42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg
│ ├── 63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg
│ ├── 6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg
│ ├── 97a19842-db31-4537-9241-5053d7c96239.3356499c.jpg
│ ├── RedisLabsIllustration.bebd0eb3.svg
│ ├── e182115a-63d2-42ce-8fe0-5f696ecdfba6.0516fcb6.jpg
│ ├── efe0c7a3-9835-4dfb-87e1-575b7d06701a.e2ac5748.jpg
│ ├── f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg
│ ├── f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg
│ └── x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg
├── index.html
└── js
│ ├── app.98a786d7.js
│ ├── app.98a786d7.js.map
│ ├── chunk-vendors.5dc46f7b.js
│ └── chunk-vendors.5dc46f7b.js.map
├── client
├── .env.example
├── .gitignore
├── .prettierrc.js
├── README.md
├── babel.config.js
├── package.json
├── public
│ ├── favicon-32x32.png
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── RedisLabsIllustration.svg
│ │ └── products
│ │ │ ├── 1f1321bb-0542-45d0-9601-2a3d007d5842.jpg
│ │ │ ├── 42860491-9f15-43d4-adeb-0db2cc99174a.jpg
│ │ │ ├── 63a3c635-4505-4588-8457-ed04fbb76511.jpg
│ │ │ ├── 6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg
│ │ │ ├── 97a19842-db31-4537-9241-5053d7c96239.jpg
│ │ │ ├── e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg
│ │ │ ├── efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg
│ │ │ ├── f5384efc-eadb-4d7b-a131-36516269c218.jpg
│ │ │ ├── f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg
│ │ │ └── x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg
│ ├── components
│ │ ├── Cart.vue
│ │ ├── CartItem.vue
│ │ ├── CartList.vue
│ │ ├── Info.vue
│ │ ├── Product.vue
│ │ ├── ProductList.vue
│ │ └── ResetDataBtn.vue
│ ├── config
│ │ └── index.js
│ ├── main.js
│ ├── plugins
│ │ ├── axios.js
│ │ └── vuetify.js
│ ├── store
│ │ ├── index.js
│ │ └── modules
│ │ │ ├── cart.js
│ │ │ └── products.js
│ └── styles
│ │ └── styles.scss
└── vue.config.js
├── docs
└── YTThumbnail.png
├── heroku.yml
├── images
└── app_preview_image.png
├── marketplace.json
├── preview.png
├── server
├── .env.example
├── .gitignore
├── .prettierrc.js
├── README.md
├── docker-compose.yml
├── package.json
├── pm2.json
└── src
│ ├── controllers
│ ├── Cart
│ │ ├── DeleteItemController.js
│ │ ├── EmptyController.js
│ │ ├── IndexController.js
│ │ └── UpdateController.js
│ └── Product
│ │ ├── IndexController.js
│ │ └── ResetController.js
│ ├── index.js
│ ├── middleware
│ └── checkSession.js
│ ├── products.json
│ ├── routes
│ ├── cart.js
│ ├── index.js
│ └── products.js
│ └── services
│ └── RedisClient.js
└── vercel.json
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | .idea
3 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine as vue-build
2 | WORKDIR /app
3 | COPY ./client/package.json ./
4 | RUN npm install
5 | COPY ./client/ .
6 | RUN npm run build
7 |
8 | FROM node:lts-alpine AS server-build
9 | WORKDIR /app/server
10 | COPY ./server/package.json ./
11 | RUN npm install
12 | COPY ./server .
13 | COPY --from=vue-build /app/dist ./../client-dist
14 | EXPOSE ${PORT}
15 | CMD ["node", "./src/index.js"]
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Redis Developer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tutorial: A Shopping Cart app in NodeJS and Redis JSON
2 |
3 | ## Technical Stack
4 |
5 | - Frontend - Vue.js
6 | - Backend - NodeJS, ExpressJS, Redis(Redis JSON)
7 |
8 | This shopping cart is using Redis and Redis JSON functionalities, allowing you to save JSON as keys using methods like json_get and json_set.
9 |
10 |
11 | ## How it works
12 |
13 | ### How the data is stored:
14 |
15 | * The products data is stored in external json file. After first request this data is saved in a JSON data type in Redis like: `JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": 10 }'`.
16 | * E.g `JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 2 }'`
17 | * The cart data is stored in a hash like: `HSET cart:{cartId} product:{productId} {productQuantity}`, where cartId is random generated value and stored in user session.
18 | * E.g `HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1`
19 |
20 | ### How the data is modified:
21 | * The product data is modified like `JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": {newStock} }'`.
22 | * E.g `JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 1 }'`
23 | * The cart data is modified like `HSET cart:{cartId} product:{productId} {newProductQuantity}` or `HINCRBY cart:{cartId} product:{productId} {incrementBy}`.
24 | * E.g `HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 2`
25 | * E.g `HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1`
26 | * E.g `HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 -1`
27 | * Product can be removed from cart like `HDEL cart:{cartId} product:{productId}`
28 | * E.g `HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`
29 | * Cart can be cleared using `HGETALL cart:{cartId}` and then `HDEL cart:{cartId} {productKey}` in loop.
30 | * E.g `HGETALL cart:77f7fc881edc2f558e683a230eac217d` => `product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `product:1f1321bb-0542-45d0-9601-2a3d007d5842` => `HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `HDEL cart:77f7fc881edc2f558e683a230eac217d product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `HDEL cart:77f7fc881edc2f558e683a230eac217d product:1f1321bb-0542-45d0-9601-2a3d007d5842`
31 | * All carts can be deleted when reset data is requested like: `SCAN {cursor} MATCH cart:*` and then `DEL cart:{cartId}` in loop.
32 | * E.g `SCAN {cursor} MATCH cart:*` => `cart:77f7fc881edc2f558e683a230eac217d`, `cart:217dedc2f558e683a230eac77f7fc881`, `cart:1ede77f558683a230eac7fc88217dc2f` => `DEL cart:77f7fc881edc2f558e683a230eac217d`, `DEL cart:217dedc2f558e683a230eac77f7fc881`, `DEL cart:1ede77f558683a230eac7fc88217dc2f`
33 |
34 | ### How the data is accessed:
35 | * Products: `SCAN {cursor} MATCH product:*` to get all product keys and then `JSON.GET {productKey}` in loop.
36 | * E.g `SCAN {cursor} MATCH product:*` => `product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `product:1f1321bb-0542-45d0-9601-2a3d007d5842` => `JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b1`, `JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842`
37 | * Cart: `HGETALL cart:{cartId}`to get quantity of products and `JSON.GET product:{productId}` to get products data in loop.
38 | * E.g `HGETALL cart:77f7fc881edc2f558e683a230eac217d` => `product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 (quantity: 1)`, `product:f9a6d214-1c38-47ab-a61c-c99a59438b12 (quantity: 0)`, `product:1f1321bb-0542-45d0-9601-2a3d007d5842 (quantity: 2)` => `JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842`
39 | * HGETALL returns array of keys and corresponding values from hash data type.
40 |
41 | ## Hot to run it locally?
42 |
43 | ### Prerequisites
44 |
45 | - Node - v12.19.0
46 | - NPM - v6.14.8
47 | - Docker - v19.03.13 (optional)
48 |
49 | ### Local installation
50 |
51 | Go to server folder (`cd ./server`) and then:
52 |
53 | ```
54 | # Environmental variables
55 |
56 | Copy `.env.example` to `.env` file and fill environmental variables
57 |
58 | REDIS_PORT: Redis port (default: 6379)
59 | REDIS_HOST: Redis host (default: 127.0.0.1)
60 | REDIS_PASSWORD: Redis password (default: demo)
61 |
62 | cp .env.example .env
63 |
64 | # Run docker compose or install redis with RedisJson module manually. You can also go to https://redislabs.com/try-free/ and obtain necessary environmental variables
65 |
66 | docker network create global
67 | docker-compose up -d --build
68 |
69 | # Install dependencies
70 |
71 | npm install
72 |
73 | # Run dev server
74 |
75 | npm run dev
76 | ```
77 |
78 | Go to client folder (`cd ./client`) and then:
79 |
80 | ```
81 | # Environmental variables
82 |
83 | Copy `.env.example` to `.env` file
84 |
85 | cp .env.example .env
86 |
87 | # Install dependencies
88 |
89 | npm install
90 |
91 | # Serve locally
92 |
93 | npm run serve
94 | ```
95 |
96 | ## Deployment
97 |
98 | To make deploys work, you need to create free account in https://redislabs.com/try-free/, create Redis instance with `RedisJson` module and get informations - REDIS_ENDPOINT_URI and REDIS_PASSWORD. You must pass them as environmental variables.
99 |
100 | ### Google Cloud Run
101 |
102 | [](https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs.git)
104 |
105 | ### Heroku
106 |
107 | [](https://heroku.com/deploy)
108 |
109 | ### Vercel
110 |
111 | [](https://vercel.com/new/git/external?repository-url=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs&env=REDIS_ENDPOINT_URI,REDIS_PASSWORD)
112 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Redis Shopping Cart",
3 | "description": "This shopping cart is using Redis and RedisJson module functionalities, allowing to save JSON as keys using methods like json_get and json_set.",
4 | "repository": "https://github.com/redis-developer/basic-redis-shopping-chart-nodejs",
5 | "keywords": [
6 | "NodeJS",
7 | "ExpressJS",
8 | "Redis",
9 | "Shopping Cart",
10 | "json_get",
11 | "json_set"
12 | ],
13 | "env": {
14 | "REDIS_ENDPOINT_URI": {
15 | "description": "Redis server URI",
16 | "required": true
17 | },
18 | "REDIS_PASSWORD": {
19 | "description": "Redis password",
20 | "required": true
21 | }
22 | },
23 | "stack": "container"
24 | }
25 |
--------------------------------------------------------------------------------
/client-dist/css/app.084652a9.css:
--------------------------------------------------------------------------------
1 | body{background-image:url(../img/RedisLabsIllustration.bebd0eb3.svg);background-color:#f8f8fb;background-repeat:no-repeat;background-size:340px;background-position:100% 0}#app{background:none}.text{word-break:normal}input{text-align:center}.v-input__slot,input{margin:0!important}.v-input__slot:before,.v-input__slot:before:active{border-style:none!important;content:none}.h-full{height:100%}
--------------------------------------------------------------------------------
/client-dist/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/favicon-32x32.png
--------------------------------------------------------------------------------
/client-dist/img/1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg
--------------------------------------------------------------------------------
/client-dist/img/42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg
--------------------------------------------------------------------------------
/client-dist/img/63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg
--------------------------------------------------------------------------------
/client-dist/img/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg
--------------------------------------------------------------------------------
/client-dist/img/97a19842-db31-4537-9241-5053d7c96239.3356499c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/97a19842-db31-4537-9241-5053d7c96239.3356499c.jpg
--------------------------------------------------------------------------------
/client-dist/img/RedisLabsIllustration.bebd0eb3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
--------------------------------------------------------------------------------
/client-dist/img/e182115a-63d2-42ce-8fe0-5f696ecdfba6.0516fcb6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/e182115a-63d2-42ce-8fe0-5f696ecdfba6.0516fcb6.jpg
--------------------------------------------------------------------------------
/client-dist/img/efe0c7a3-9835-4dfb-87e1-575b7d06701a.e2ac5748.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/efe0c7a3-9835-4dfb-87e1-575b7d06701a.e2ac5748.jpg
--------------------------------------------------------------------------------
/client-dist/img/f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg
--------------------------------------------------------------------------------
/client-dist/img/f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg
--------------------------------------------------------------------------------
/client-dist/img/x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg
--------------------------------------------------------------------------------
/client-dist/index.html:
--------------------------------------------------------------------------------
1 |
Redis Shopping Cart We're sorry but redis-shopping-cart-client doesn't work properly without JavaScript enabled. Please enable it to continue.
--------------------------------------------------------------------------------
/client-dist/js/app.98a786d7.js:
--------------------------------------------------------------------------------
1 | (function(t){function e(e){for(var r,s,o=e[0],i=e[1],d=e[2],l=0,f=[];l0?1:t.name.localeCompare(e.name)<0?-1:0})),a("setProducts",n),e.abrupt("return",n);case 8:case"end":return e.stop()}}),e)})))()},reset:function(t){return Object(n["a"])(regeneratorRuntime.mark((function e(){var a;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return a=t.dispatch,e.next=3,u.post("/api/products/reset");case 3:return e.next=5,a("fetch");case 5:return e.next=7,a("cart/fetch",null,{root:!0});case 7:case"end":return e.stop()}}),e)})))()}},b={state:f,getters:p,mutations:v,actions:m,namespaced:!0},_={items:[]},h=function(){return _},g={getItems:function(t){return t.items}},x={setItems:function(t,e){t.items=e}},C={fetch:function(t){return Object(n["a"])(regeneratorRuntime.mark((function e(){var a,r,c;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return a=t.commit,e.next=3,u.get("/api/cart");case 3:return r=e.sent,c=r.data,a("setItems",c),e.abrupt("return",c);case 7:case"end":return e.stop()}}),e)})))()},save:function(t,e){return Object(n["a"])(regeneratorRuntime.mark((function a(){var r,c,n,s,o,i;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return r=t.dispatch,c=e.id,n=e.quantity,s=e.incrementBy,a.next=4,u.put("/api/cart/".concat(c),{quantity:n,incrementBy:s});case 4:return o=a.sent,i=o.data,a.next=8,r("fetch");case 8:return a.next=10,r("products/fetch",null,{root:!0});case 10:return a.abrupt("return",i);case 11:case"end":return a.stop()}}),a)})))()},delete:function(t,e){return Object(n["a"])(regeneratorRuntime.mark((function a(){var r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return r=t.dispatch,a.next=3,u.delete("/api/cart/".concat(e));case 3:return a.next=5,r("fetch");case 5:return a.next=7,r("products/fetch",null,{root:!0});case 7:case"end":return a.stop()}}),a)})))()},empty:function(t){return Object(n["a"])(regeneratorRuntime.mark((function e(){var a;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return a=t.dispatch,e.next=3,u.delete("/api/cart");case 3:return e.next=5,a("fetch");case 5:return e.next=7,a("products/fetch",null,{root:!0});case 7:case"end":return e.stop()}}),e)})))()}},w={state:h,getters:g,mutations:x,actions:C,namespaced:!0};r["a"].use(c["a"]);var y=new c["a"].Store({state:{},getters:{},mutations:{},actions:{},modules:{products:b,cart:w}}),j=a("f309");r["a"].use(j["a"]);var O=new j["a"]({}),k=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-app",[a("v-container",[a("div",{staticClass:"my-8 d-flex align-center"},[a("div",{staticClass:"pa-4 rounded-lg red darken-1"},[a("v-icon",{attrs:{color:"white",size:"45"}},[t._v("mdi-cart-plus")])],1),a("h1",{staticClass:"ml-6 font-weight-regular"},[t._v("Shopping Cart demo")])])]),a("v-container",[a("v-row",[a("v-col",{attrs:{cols:"12",sm:"7",md:"8"}},[a("info"),a("product-list",{attrs:{products:t.products}})],1),a("v-col",{staticClass:"d-flex flex-column",attrs:{cols:"12",sm:"5",md:"4"}},[a("cart"),a("reset-data-btn",{staticClass:"mt-6"})],1)],1),a("v-footer",{staticClass:"mt-12 pa-0"},[t._v(" © Copyright 2021 | All Rights Reserved to Redis Labs ")])],1)],1)},I=[],E=a("5530"),V=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-card",{staticClass:"mr-md-4",attrs:{elevation:"5",dark:""}},[a("v-card-title",{staticClass:"pa-3"},[a("v-icon",{staticClass:"mr-2"},[t._v("mdi-cart")]),t._v(" Shopping cart ")],1),t.items.length?a("v-card-text",{staticClass:"pa-3"},[a("cart-list",{attrs:{items:t.items}}),a("v-divider",{staticClass:"mt-6 mb-2"}),a("div",{staticClass:"text-right text title"},[t._v(" Total: "),a("span",{staticClass:"font-weight-black"},[t._v("$"+t._s(t.total))])])],1):a("v-card-text",{staticClass:"pa-3 text-center"},[a("v-icon",{attrs:{"x-large":""}},[t._v("mdi-cart")]),a("p",[t._v(" Cart is Empty. Please add items. ")])],1),a("v-card-actions",{staticClass:"pa-3 justify-space-between"},[a("v-btn",{attrs:{outlined:"",color:"orange"},on:{click:t.emptyCart}},[a("span",{staticClass:"d-xs-flex d-none d-xl-flex"},[t._v("Clear cart")]),a("v-icon",{attrs:{right:"",dark:""}},[t._v(" mdi-close-circle-outline ")])],1),a("v-btn",{staticClass:"primary"},[t._v(" Checkout "),a("v-icon",{attrs:{right:"",dark:""}},[t._v(" mdi-check ")])],1)],1)],1)},T=[],S=(a("d81d"),a("13d5"),function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-row",t._l(t.items,(function(e){return a("v-col",{key:e.id,attrs:{cols:"12"}},[a("cart-item",{attrs:{item:e},on:{save:t.save,delete:t.remove}})],1)})),1)}),R=[],N=(a("7db0"),function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("v-card",{staticClass:"secondary rounded-lg px-2 pr-lg-2 pl-lg-0 mb-2"},[r("v-row",[r("v-col",{staticClass:"py-0 d-lg-flex d-sm-none d-md-none pl-0 pl-sm-3 pl-md-3 pl-lg-3",attrs:{cols:"4",lg:"3",md:"0"}},[r("v-img",{staticClass:"rounded-lg d-lg-flex d-md-none",attrs:{"min-height":"100%",src:a("2204")("./"+t.item.id+".jpg")}})],1),r("v-col",{attrs:{cols:"8",lg:"9",md:"12",sm:"12"}},[r("v-card-title",{staticClass:"text-subtitle-1 text-xl-h6 pa-0"},[t._v(" "+t._s(t.item.name)+" ")]),r("v-card-actions",{staticClass:"justify-space-between text-xl-h6 px-0"},[t._v(" $"+t._s(t.item.priceSum)+" "),r("v-btn-toggle",{staticClass:"secondary",attrs:{multiple:"",rounded:""}},[r("v-btn",{attrs:{small:""},on:{click:function(e){return t.incrementItem(-1)}}},[t._v(" - ")]),r("v-btn",{attrs:{small:""}},[r("v-text-field",{staticStyle:{"max-width":"10px"},on:{input:t.onItemQuantityChange},model:{value:t.itemQuantity,callback:function(e){t.itemQuantity=e},expression:"itemQuantity"}})],1),r("v-btn",{attrs:{disabled:!t.item.stock,small:""},on:{click:function(e){return t.incrementItem(1)}}},[t._v(" + ")])],1)],1)],1)],1)],1)}),q=[],H={name:"CartItem",props:{item:{type:Object,required:!0}},data:function(){return{itemQuantity:0}},watch:{item:{immediate:!0,handler:function(t){this.itemQuantity=parseInt(t.quantity)}}},methods:{onItemQuantityChange:function(){this.$emit("save",{id:this.item.id,quantity:this.itemQuantity})},deleteItem:function(t){this.$emit("delete",t)},incrementItem:function(t){this.itemQuantity+t!==0?this.$emit("save",{id:this.item.id,incrementBy:t}):this.deleteItem(this.item.id)}}},L=H,A=a("2877"),P=a("6544"),D=a.n(P),B=a("8336"),J=a("a609"),$=a("b0af"),G=a("99d9"),Q=a("62ad"),M=a("adda"),U=a("0fd9"),F=a("8654"),Y=Object(A["a"])(L,N,q,!1,null,null,null),K=Y.exports;D()(Y,{VBtn:B["a"],VBtnToggle:J["a"],VCard:$["a"],VCardActions:G["a"],VCardTitle:G["d"],VCol:Q["a"],VImg:M["a"],VRow:U["a"],VTextField:F["a"]});var W={name:"CartList",components:{CartItem:K},props:{items:{type:Array,required:!1,defaultValue:function(){return[]}}},computed:Object(E["a"])({},Object(c["c"])({products:"products/getProducts"})),methods:Object(E["a"])(Object(E["a"])({},Object(c["b"])({saveItems:"cart/save",deleteItem:"cart/delete"})),{},{save:function(t){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function a(){var r,c,n;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:if(a.prev=0,""!==t.quantity){a.next=3;break}return a.abrupt("return");case 3:if(!t.quantity){a.next=14;break}if(t.quantity=parseInt(t.quantity),0!==t.quantity){a.next=9;break}return a.next=8,e.remove(t.id);case 8:return a.abrupt("return");case 9:if(r=e.items.find((function(e){return e.id===t.id})),c=e.products.find((function(e){return e.id===t.id})),n=parseInt(r.quantity),!(t.quantity>n&&t.quantity>n+c.stock)){a.next=14;break}return a.abrupt("return");case 14:return a.next=16,e.saveItems(t);case 16:a.next=21;break;case 18:a.prev=18,a.t0=a["catch"](0),console.error(a.t0);case 21:case"end":return a.stop()}}),a,null,[[0,18]])})))()},remove:function(t){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,e.deleteItem(t);case 3:a.next=8;break;case 5:a.prev=5,a.t0=a["catch"](0),console.error(a.t0);case 8:case"end":return a.stop()}}),a,null,[[0,5]])})))()}})},z=W,X=Object(A["a"])(z,S,R,!1,null,null,null),Z=X.exports;D()(X,{VCol:Q["a"],VRow:U["a"]});var tt={name:"Cart",components:{CartList:Z},computed:Object(E["a"])(Object(E["a"])({},Object(c["c"])({cartItems:"cart/getItems"})),{},{items:function(){return this.cartItems?this.cartItems.map((function(t){var e=t.quantity,a=t.product;return Object(E["a"])(Object(E["a"])({},a),{},{quantity:e,priceSum:e*a.price})})):[]},total:function(){return this.cartItems?this.cartItems.reduce((function(t,e){var a=e.quantity,r=e.product.price;return t+a*r}),0):0}}),created:function(){var t=this;return Object(n["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.fetchCart();case 2:case"end":return e.stop()}}),e)})))()},methods:Object(E["a"])({},Object(c["b"])({fetchCart:"cart/fetch",emptyCart:"cart/empty"}))},et=tt,at=a("ce7e"),rt=a("132d"),ct=Object(A["a"])(et,V,T,!1,null,null,null),nt=ct.exports;D()(ct,{VBtn:B["a"],VCard:$["a"],VCardActions:G["a"],VCardText:G["c"],VCardTitle:G["d"],VDivider:at["a"],VIcon:rt["a"]});var st=function(){var t=this,e=t.$createElement,a=t._self._c||e;return t.products.length?a("v-row",{attrs:{align:"stretch"}},t._l(t.products,(function(e){return a("product",{key:e.id,attrs:{product:e},on:{add:t.addToCart}})})),1):a("v-row",[a("p",[t._v(" No products in store ")])])},ot=[],it=function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("v-col",{attrs:{cols:"6",sm:"6",md:"6",lg:"4"}},[r("v-card",{staticClass:"h-full",attrs:{disabled:0===t.product.stock}},[r("div",{staticClass:"d-flex justify-center"},[r("v-img",{attrs:{"max-width":"65%",src:a("2204")("./"+t.product.id+".jpg")}})],1),r("v-card-title",{staticClass:"pa-3 text-subtitle-1 text-xl-h6"},[t._v(" "+t._s(t.product.name)+" ")]),r("v-card-subtitle",{staticClass:"pa-3 text-subtitle-1 text-xl-h6"},[t._v(" $"+t._s(t.product.price)+" ")]),r("v-card-text",{staticClass:"pa-3 text-left text caption"}),r("v-divider"),r("v-card-actions",{staticClass:"pa-3 justify-space-between"},[r("span",[t._v(t._s(t.product.stock?t.product.stock+" in":"out of")+" stock")]),r("v-btn",{staticClass:"success",attrs:{disabled:0===t.product.stock},on:{click:function(e){return t.$emit("add",t.product.id)}}},[r("span",{staticClass:"d-xs-flex d-none d-xl-flex"},[t._v("Add to cart")]),r("v-icon",{attrs:{right:"",dark:""}},[t._v("mdi-cart-plus")])],1)],1)],1)],1)},dt=[],ut={name:"Product",props:{product:{type:Object,required:!0}}},lt=ut,ft=Object(A["a"])(lt,it,dt,!1,null,null,null),pt=ft.exports;D()(ft,{VBtn:B["a"],VCard:$["a"],VCardActions:G["a"],VCardSubtitle:G["b"],VCardText:G["c"],VCardTitle:G["d"],VCol:Q["a"],VDivider:at["a"],VIcon:rt["a"],VImg:M["a"]});var vt={name:"ProductList",props:{products:{type:Array,required:!1,defaultValue:function(){return[]}}},components:{Product:pt},methods:Object(E["a"])(Object(E["a"])({},Object(c["b"])({saveItem:"cart/save"})),{},{addToCart:function(t){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,e.saveItem({id:t,incrementBy:1});case 3:a.next=8;break;case 5:a.prev=5,a.t0=a["catch"](0),console.error(a.t0);case 8:case"end":return a.stop()}}),a,null,[[0,5]])})))()}})},mt=vt,bt=Object(A["a"])(mt,st,ot,!1,null,null,null),_t=bt.exports;D()(bt,{VRow:U["a"]});var ht=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-btn",{staticClass:"mx-auto error",on:{click:t.resetData}},[t._v(" Reset data "),a("v-icon",{attrs:{right:"",dark:""}},[t._v("mdi-restore")])],1)},gt=[],xt={computed:Object(E["a"])({},Object(c["c"])({cartItems:"cart/getItems"})),methods:Object(E["a"])(Object(E["a"])({},Object(c["b"])({reset:"products/reset"})),{},{resetData:function(){var t=this;return Object(n["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.reset();case 3:e.next=8;break;case 5:e.prev=5,e.t0=e["catch"](0),console.error(e.t0);case 8:case"end":return e.stop()}}),e,null,[[0,5]])})))()}})},Ct=xt,wt=Object(A["a"])(Ct,ht,gt,!1,null,null,null),yt=wt.exports;D()(wt,{VBtn:B["a"],VIcon:rt["a"]});var jt=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-alert",{staticClass:"mb-6",attrs:{text:"",dense:"",color:"info",border:"left"}},[a("v-row",{attrs:{align:"center","no-gutters":""},on:{click:function(e){t.alert=!t.alert}}},[a("v-col",{staticClass:"grow"},[a("h3",{staticClass:"headline"},[t._v(" How it works? ")])]),a("v-col",{staticClass:"shrink"},[a("v-btn",{attrs:{color:"info",outlined:""}},[t._v(" "+t._s(t.alert?"Collapse":"View more")+" ")])],1)],1),a("v-divider",{directives:[{name:"show",rawName:"v-show",value:t.alert,expression:"alert"}],staticClass:"my-4 info",staticStyle:{opacity:"0.22"}}),a("div",{directives:[{name:"show",rawName:"v-show",value:t.alert,expression:"alert"}]},[a("ol",[a("li",[t._v("How the data is stored:")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("The products data is stored in external json file. After first request this data is saved in a JSON data type in Redis like: "),a("code",[t._v('JSON.SET product:{productId} . JSON.SET product:{productId} . \'{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": 10 }\'')]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v('JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . \'{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 2 }\'')])])]),a("li",[t._v("The cart data is stored in a hash like: "),a("code",[t._v("HSET cart:{cartId} product:{productId} {productQuantity}")]),t._v(", where cartId is random generated value and stored in user session.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1")])])])]),a("li",[t._v("How the data is modified:")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("The product data is modified like "),a("code",[t._v('JSON.SET product:{productId} . \'{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": {newStock} }\'')]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v('JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . \'{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 1 }')])])]),a("li",[t._v("The cart data is modified like "),a("code",[t._v("HSET cart:{cartId} product:{productId} {newProductQuantity}")]),t._v(" or "),a("code",[t._v("HINCRBY cart:{cartId} product:{productId} {incrementBy}")]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 2")])]),a("li",[t._v("E.g "),a("code",[t._v("HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1")])]),a("li",[t._v("E.g "),a("code",[t._v("HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 -1")])])]),a("li",[t._v("Product can be removed from cart like "),a("code",[t._v("HDEL cart:{cartId} product:{productId}")]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")])])]),a("li",[t._v("Cart can be cleared using "),a("code",[t._v("HGETALL cart:{cartId}")]),t._v(" and then "),a("code",[t._v("HDEL cart:{cartId} {productKey}")]),t._v(" in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HGETALL cart:77f7fc881edc2f558e683a230eac217d")]),t._v(" => "),a("code",[t._v("product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("product:1f1321bb-0542-45d0-9601-2a3d007d5842")]),t._v(" => "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:1f1321bb-0542-45d0-9601-2a3d007d5842")])])]),a("li",[t._v("All carts can be deleted when reset data is requested like: "),a("code",[t._v("SCAN {cursor} MATCH cart:*")]),t._v(" and then "),a("code",[t._v("DEL cart:{cartId}")]),t._v(" in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("SCAN {cursor} MATCH cart:*")]),t._v(" => "),a("code",[t._v("cart:77f7fc881edc2f558e683a230eac217d")]),t._v(", "),a("code",[t._v("cart:217dedc2f558e683a230eac77f7fc881")]),t._v(", "),a("code",[t._v("cart:1ede77f558683a230eac7fc88217dc2f")]),t._v(" => "),a("code",[t._v("DEL cart:77f7fc881edc2f558e683a230eac217d")]),t._v(", "),a("code",[t._v("DEL cart:217dedc2f558e683a230eac77f7fc881")]),t._v(", "),a("code",[t._v("DEL cart:1ede77f558683a230eac7fc88217dc2f")])])])]),a("li",[t._v("How the data is accessed:")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("Products: "),a("code",[t._v("SCAN {cursor} MATCH product:*")]),t._v(" to get all product keys and then "),a("code",[t._v("JSON.GET {productKey}")]),t._v(" in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("SCAN {cursor} MATCH product:*")]),t._v(" => "),a("code",[t._v("product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("product:1f1321bb-0542-45d0-9601-2a3d007d5842")]),t._v(" => "),a("code",[t._v("JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b1")]),t._v(", "),a("code",[t._v("JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842")])])]),a("li",[t._v("Cart: "),a("code",[t._v("HGETALL cart:{cartId}")]),t._v(" to get quantity of products and "),a("code",[t._v("JSON.GET product:{productId}")]),t._v(" to get products data in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HGETALL cart:77f7fc881edc2f558e683a230eac217d")]),t._v(" => "),a("code",[t._v("product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 (quantity: 1)")]),t._v(", "),a("code",[t._v("product:f9a6d214-1c38-47ab-a61c-c99a59438b12 (quantity: 0)")]),t._v(", "),a("code",[t._v("product:1f1321bb-0542-45d0-9601-2a3d007d5842 (quantity: 2)")]),t._v(" => "),a("code",[t._v("JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842")])])])])])])],1)},Ot=[],kt={data:function(){return{alert:!1}}},It=kt,Et=a("0798"),Vt=Object(A["a"])(It,jt,Ot,!1,null,null,null),Tt=Vt.exports;D()(Vt,{VAlert:Et["a"],VBtn:B["a"],VCol:Q["a"],VDivider:at["a"],VRow:U["a"]});var St={name:"App",components:{ProductList:_t,Cart:nt,ResetDataBtn:yt,Info:Tt},computed:Object(E["a"])({},Object(c["c"])({products:"products/getProducts"})),created:function(){var t=this;return Object(n["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.fetchProducts();case 2:case"end":return e.stop()}}),e)})))()},methods:Object(E["a"])({},Object(c["b"])({fetchProducts:"products/fetch"}))},Rt=St,Nt=a("7496"),qt=a("a523"),Ht=a("553a"),Lt=Object(A["a"])(Rt,k,I,!1,null,null,null),At=Lt.exports;D()(Lt,{VApp:Nt["a"],VCol:Q["a"],VContainer:qt["a"],VFooter:Ht["a"],VIcon:rt["a"],VRow:U["a"]});a("3c61");r["a"].config.productionTip=!1,new r["a"]({vuetify:O,store:y,render:function(t){return t(At)}}).$mount("#app")},"6fde":function(t,e,a){t.exports=a.p+"img/f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg"},"74a0":function(t,e,a){t.exports=a.p+"img/x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg"},7862:function(t,e,a){t.exports=a.p+"img/f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg"},"79bb":function(t,e,a){t.exports=a.p+"img/63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg"},eb8f:function(t,e,a){t.exports=a.p+"img/1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg"},ec1e:function(t,e,a){t.exports=a.p+"img/42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg"},f005:function(t,e,a){t.exports=a.p+"img/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg"},f121:function(t,e,a){t.exports={apiUrl:"http://localhost:3000"}}});
2 | //# sourceMappingURL=app.98a786d7.js.map
--------------------------------------------------------------------------------
/client/.env.example:
--------------------------------------------------------------------------------
1 | VUE_APP_API_URL=http://localhost:3000
2 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | node_modules
3 | package-lock.json
4 | /dist
5 | .env
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | pnpm-debug.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/client/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | tabWidth: 4,
4 | useTabs: false,
5 | semi: true,
6 | singleQuote: true,
7 | trailingComma: 'none',
8 | bracketSpacing: true,
9 | jsxBracketSameLine: false,
10 | proseWrap: 'always',
11 | htmlWhitespaceSensitivity: 'strict',
12 | endOfLine: 'lf',
13 | arrowParens: 'avoid'
14 | };
15 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Redis shopping cart UI
2 |
3 | ## Development
4 |
5 | ```
6 | # Environmental variables
7 |
8 | Copy `.env.example` to `.env`
9 |
10 | cp .env.example .env
11 |
12 | # Install dependencies
13 |
14 | npm install
15 |
16 | # Serve locally
17 |
18 | npm run serve
19 | ```
20 |
--------------------------------------------------------------------------------
/client/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redis-shopping-cart-client",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "This shopping cart is using Redis and RedisJson module functionalities, allowing to save JSON as keys using methods like json_get and json_set.",
6 | "keywords": [
7 | "VueJS",
8 | "Shopping Cart"
9 | ],
10 | "author": "RemoteCraftsmen.com",
11 | "scripts": {
12 | "serve": "vue-cli-service serve",
13 | "build": "vue-cli-service build",
14 | "lint": "vue-cli-service lint",
15 | "test": "echo \"Error: no test specified\" && exit 1"
16 | },
17 | "dependencies": {
18 | "axios": "^0.21.1",
19 | "core-js": "^3.6.5",
20 | "vue": "^2.6.12",
21 | "vuelidate": "^0.7.6",
22 | "vuetify": "^2.4.0",
23 | "vuex": "^3.6.0"
24 | },
25 | "devDependencies": {
26 | "@vue/cli-plugin-babel": "~4.5.0",
27 | "@vue/cli-plugin-eslint": "~4.5.0",
28 | "@vue/cli-service": "~4.5.0",
29 | "babel-eslint": "^10.1.0",
30 | "eslint": "^7.17.0",
31 | "eslint-plugin-prettier": "^3.3.1",
32 | "eslint-plugin-vue": "^7.4.1",
33 | "prettier": "^2.2.1",
34 | "sass": "^1.32.0",
35 | "sass-loader": "^10.1.0",
36 | "vue-cli-plugin-vuetify": "~2.0.9",
37 | "vue-template-compiler": "^2.6.11",
38 | "vuetify-loader": "^1.6.0"
39 | },
40 | "eslintConfig": {
41 | "root": true,
42 | "env": {
43 | "node": true
44 | },
45 | "extends": [
46 | "plugin:vue/essential",
47 | "eslint:recommended"
48 | ],
49 | "parserOptions": {
50 | "parser": "babel-eslint"
51 | },
52 | "rules": {}
53 | },
54 | "browserslist": [
55 | "> 1%",
56 | "last 2 versions",
57 | "not dead"
58 | ],
59 | "_id": "redis-shopping-cart-client@1.0.0",
60 | "license": "MIT"
61 | }
62 |
--------------------------------------------------------------------------------
/client/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/public/favicon-32x32.png
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Redis Shopping Cart
9 |
13 |
17 |
18 |
19 |
20 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't
22 | work properly without JavaScript enabled. Please enable it to
23 | continue.
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mdi-cart-plus
7 |
8 |
Shopping Cart demo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | © Copyright 2021 | All Rights Reserved to Redis Labs
26 |
27 |
28 |
29 |
30 |
31 |
65 |
--------------------------------------------------------------------------------
/client/src/assets/RedisLabsIllustration.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
--------------------------------------------------------------------------------
/client/src/assets/products/1f1321bb-0542-45d0-9601-2a3d007d5842.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/1f1321bb-0542-45d0-9601-2a3d007d5842.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/42860491-9f15-43d4-adeb-0db2cc99174a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/42860491-9f15-43d4-adeb-0db2cc99174a.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/63a3c635-4505-4588-8457-ed04fbb76511.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/63a3c635-4505-4588-8457-ed04fbb76511.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/97a19842-db31-4537-9241-5053d7c96239.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/97a19842-db31-4537-9241-5053d7c96239.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/f5384efc-eadb-4d7b-a131-36516269c218.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/f5384efc-eadb-4d7b-a131-36516269c218.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg
--------------------------------------------------------------------------------
/client/src/assets/products/x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg
--------------------------------------------------------------------------------
/client/src/components/Cart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-cart
5 | Shopping cart
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Total: ${{ total }}
15 |
16 |
17 |
18 |
19 | mdi-cart
20 | Cart is Empty. Please add items.
21 |
22 |
23 |
24 |
29 | Clear cart
30 | mdi-close-circle-outline
31 |
32 |
33 | Checkout
34 | mdi-check
35 |
36 |
37 |
38 |
39 |
40 |
87 |
--------------------------------------------------------------------------------
/client/src/components/CartItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 | {{ item.name }}
15 |
16 |
17 |
18 | ${{ item.priceSum }}
19 |
20 |
25 |
26 | -
27 |
28 |
29 |
30 |
35 |
36 |
37 |
42 | +
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
104 |
--------------------------------------------------------------------------------
/client/src/components/CartList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
87 |
--------------------------------------------------------------------------------
/client/src/components/Info.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
15 | How it works?
16 |
17 |
18 |
19 |
23 | {{ alert ? 'Collapse' : 'View more'}}
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 | How the data is stored:
36 |
37 | The products data is stored in external json file. After first request this data is saved in a JSON data type in Redis like: JSON.SET product:{productId} . JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": 10 }'
.
38 |
39 | E.g JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 2 }'
40 |
41 | The cart data is stored in a hash like: HSET cart:{cartId} product:{productId} {productQuantity}
, where cartId is random generated value and stored in user session.
42 |
43 | E.g HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1
44 |
45 |
46 |
47 | How the data is modified:
48 |
49 | The product data is modified like JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": {newStock} }'
.
50 |
51 | E.g JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 1 }
52 |
53 | The cart data is modified like HSET cart:{cartId} product:{productId} {newProductQuantity}
or HINCRBY cart:{cartId} product:{productId} {incrementBy}
.
54 |
55 | E.g HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 2
56 | E.g HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1
57 | E.g HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 -1
58 |
59 | Product can be removed from cart like HDEL cart:{cartId} product:{productId}
.
60 |
61 | E.g HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
62 |
63 | Cart can be cleared using HGETALL cart:{cartId}
and then HDEL cart:{cartId} {productKey}
in loop.
64 |
65 | E.g HGETALL cart:77f7fc881edc2f558e683a230eac217d
=> product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
, product:f9a6d214-1c38-47ab-a61c-c99a59438b12
, product:1f1321bb-0542-45d0-9601-2a3d007d5842
=> HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
, HDEL cart:77f7fc881edc2f558e683a230eac217d product:f9a6d214-1c38-47ab-a61c-c99a59438b12
, HDEL cart:77f7fc881edc2f558e683a230eac217d product:1f1321bb-0542-45d0-9601-2a3d007d5842
66 |
67 | All carts can be deleted when reset data is requested like: SCAN {cursor} MATCH cart:*
and then DEL cart:{cartId}
in loop.
68 |
69 | E.g SCAN {cursor} MATCH cart:*
=> cart:77f7fc881edc2f558e683a230eac217d
, cart:217dedc2f558e683a230eac77f7fc881
, cart:1ede77f558683a230eac7fc88217dc2f
=> DEL cart:77f7fc881edc2f558e683a230eac217d
, DEL cart:217dedc2f558e683a230eac77f7fc881
, DEL cart:1ede77f558683a230eac7fc88217dc2f
70 |
71 |
72 |
73 | How the data is accessed:
74 |
75 | Products: SCAN {cursor} MATCH product:*
to get all product keys and then JSON.GET {productKey}
in loop.
76 |
77 | E.g SCAN {cursor} MATCH product:*
=> product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
, product:f9a6d214-1c38-47ab-a61c-c99a59438b12
, product:1f1321bb-0542-45d0-9601-2a3d007d5842
=> JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
, JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b1
, JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842
78 |
79 | Cart: HGETALL cart:{cartId}
to get quantity of products and JSON.GET product:{productId}
to get products data in loop.
80 |
81 | E.g HGETALL cart:77f7fc881edc2f558e683a230eac217d
=> product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 (quantity: 1)
, product:f9a6d214-1c38-47ab-a61c-c99a59438b12 (quantity: 0)
, product:1f1321bb-0542-45d0-9601-2a3d007d5842 (quantity: 2)
=> JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
, JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b12
, JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
98 |
--------------------------------------------------------------------------------
/client/src/components/Product.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 | {{ product.name }}
13 |
14 |
15 |
16 | ${{ product.price }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ product.stock ? `${product.stock} in` : 'out of' }} stock
25 |
26 |
31 | Add to cart
32 | mdi-cart-plus
33 |
34 |
35 |
36 |
37 |
38 |
39 |
51 |
--------------------------------------------------------------------------------
/client/src/components/ProductList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | No products in store
12 |
13 |
14 |
15 |
49 |
--------------------------------------------------------------------------------
/client/src/components/ResetDataBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Reset data
4 | mdi-restore
5 |
6 |
7 |
8 |
31 |
--------------------------------------------------------------------------------
/client/src/config/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apiUrl: process.env.VUE_APP_API_URL || ''
3 | };
4 |
--------------------------------------------------------------------------------
/client/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import store from './store';
3 | import vuetify from './plugins/vuetify';
4 | import App from './App.vue';
5 | import './styles/styles.scss';
6 |
7 | Vue.config.productionTip = false;
8 |
9 | new Vue({
10 | vuetify,
11 | store,
12 | render: h => h(App)
13 | }).$mount('#app');
14 |
--------------------------------------------------------------------------------
/client/src/plugins/axios.js:
--------------------------------------------------------------------------------
1 | import globalAxios from 'axios';
2 | import { apiUrl } from '../config';
3 |
4 | const axios = globalAxios.create({
5 | baseURL: apiUrl,
6 | withCredentials: true
7 | });
8 |
9 | export default axios;
10 |
--------------------------------------------------------------------------------
/client/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuetify from 'vuetify/lib/framework';
3 |
4 | Vue.use(Vuetify);
5 |
6 | export default new Vuetify({
7 | });
8 |
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | import products from './modules/products';
5 | import cart from './modules/cart';
6 |
7 | Vue.use(Vuex);
8 |
9 | export default new Vuex.Store({
10 | state: {},
11 | getters: {},
12 | mutations: {},
13 | actions: {},
14 | modules: {
15 | products,
16 | cart
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/client/src/store/modules/cart.js:
--------------------------------------------------------------------------------
1 | import axios from '@/plugins/axios';
2 |
3 | const initialState = {
4 | items: []
5 | };
6 |
7 | const state = () => initialState;
8 |
9 | const getters = {
10 | getItems: state => state.items
11 | };
12 |
13 | const mutations = {
14 | setItems: (state, items) => {
15 | state.items = items;
16 | }
17 | };
18 |
19 | const actions = {
20 | async fetch({ commit }) {
21 | const { data } = await axios.get('/api/cart');
22 |
23 | commit('setItems', data);
24 |
25 | return data;
26 | },
27 | async save({ dispatch }, { id, quantity, incrementBy }) {
28 | const { data } = await axios.put(`/api/cart/${id}`, {
29 | quantity,
30 | incrementBy
31 | });
32 |
33 | await dispatch('fetch');
34 | await dispatch('products/fetch', null, { root: true });
35 |
36 | return data;
37 | },
38 | async delete({ dispatch }, id) {
39 | await axios.delete(`/api/cart/${id}`);
40 |
41 | await dispatch('fetch');
42 | await dispatch('products/fetch', null, { root: true });
43 | },
44 | async empty({ dispatch }) {
45 | await axios.delete('/api/cart');
46 |
47 | await dispatch('fetch');
48 | await dispatch('products/fetch', null, { root: true });
49 | }
50 | };
51 |
52 | export default {
53 | state,
54 | getters,
55 | mutations,
56 | actions,
57 | namespaced: true
58 | };
59 |
--------------------------------------------------------------------------------
/client/src/store/modules/products.js:
--------------------------------------------------------------------------------
1 | import axios from '@/plugins/axios';
2 |
3 | const initialState = {
4 | products: []
5 | };
6 |
7 | const state = () => initialState;
8 |
9 | const getters = {
10 | getProducts: state => state.products
11 | };
12 |
13 | const mutations = {
14 | setProducts: (state, products) => {
15 | state.products = products;
16 | }
17 | };
18 |
19 | const actions = {
20 | async fetch({ commit }) {
21 | const { data } = await axios.get('/api/products');
22 |
23 | const sorted = data.sort((a, b) => {
24 | if (a.name.localeCompare(b.name) > 0) {
25 | return 1;
26 | }
27 |
28 | if (a.name.localeCompare(b.name) < 0) {
29 | return -1;
30 | }
31 |
32 | return 0;
33 | });
34 |
35 | commit('setProducts', sorted);
36 |
37 | return sorted;
38 | },
39 |
40 | async reset({ dispatch }) {
41 | await axios.post('/api/products/reset');
42 |
43 | await dispatch('fetch');
44 | await dispatch('cart/fetch', null, { root: true });
45 | }
46 | };
47 |
48 | export default {
49 | state,
50 | getters,
51 | mutations,
52 | actions,
53 | namespaced: true
54 | };
55 |
--------------------------------------------------------------------------------
/client/src/styles/styles.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-image: url('../assets/RedisLabsIllustration.svg');
3 | background-color: #f8f8fb;
4 | background-repeat: no-repeat;
5 | background-size: 340px;
6 | background-position: right top;
7 | }
8 |
9 | #app {
10 | background: none;
11 | }
12 |
13 | .text {
14 | word-break: normal;
15 | }
16 |
17 | input {
18 | text-align: center;
19 | margin: 0 !important;
20 | }
21 |
22 | .v-input__slot {
23 | margin: 0 !important;
24 | }
25 |
26 | .v-input__slot::before {
27 | border-style: none !important;
28 | content: none;
29 | }
30 |
31 | .v-input__slot::before:active {
32 | border-style: none !important;
33 | content: none;
34 | }
35 |
36 | .h-full {
37 | height: 100%;
38 | }
39 |
40 | .v-sheet.v-card:not(.v-sheet--outlined) {
41 | box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%),
42 | 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%) !important;
43 | }
44 |
--------------------------------------------------------------------------------
/client/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | transpileDependencies: ['vuetify'],
5 | configureWebpack: {
6 | resolve: {
7 | alias: {
8 | '@': path.resolve(__dirname, 'src/')
9 | }
10 | }
11 | },
12 | pluginOptions: {
13 | i18n: {
14 | locale: 'en',
15 | fallbackLocale: 'en',
16 | localeDir: 'locales',
17 | enableInSFC: true
18 | }
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/docs/YTThumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/docs/YTThumbnail.png
--------------------------------------------------------------------------------
/heroku.yml:
--------------------------------------------------------------------------------
1 | build:
2 | docker:
3 | web: Dockerfile
4 |
--------------------------------------------------------------------------------
/images/app_preview_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/images/app_preview_image.png
--------------------------------------------------------------------------------
/marketplace.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_name": "Shopping Cart app in NodeJS",
3 | "description": "Shopping Cart app in NodeJS using Redis JSON feature functionalities",
4 | "type": "Building Block",
5 | "contributed_by": "Redis",
6 | "repo_url": "https://github.com/redis-developer/basic-redis-shopping-chart-nodejs",
7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/master/images/app_preview_image.png",
8 | "download_url": "https://github.com/redis-developer/basic-redis-shopping-chart-nodejs/archive/main.zip",
9 | "hosted_url": "",
10 | "quick_deploy": "true",
11 | "deploy_buttons": [
12 | {
13 | "heroku": "https://heroku.com/deploy?template=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs"
14 | },
15 | {
16 | "vercel": "https://vercel.com/new/git/external?repository-url=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs&env=REDIS_ENDPOINT_URI,REDIS_PASSWORD&envDescription=REDIS_ENDPOINT_URI%20is%20required%20at%20least%20to%20connect%20to%20Redis%20clouding%20server"
17 | },
18 | {
19 | "Google": "https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs.git"
20 | }
21 | ],
22 | "language": [
23 | "JavaScript",
24 | "NodeJS"
25 | ],
26 | "redis_commands": [
27 | "JSON_GET",
28 | "JSON_SET",
29 | "HGETALL",
30 | "HSET",
31 | "HGET",
32 | "HDEL",
33 | "HINCRBY",
34 | "DEL",
35 | "SCAN"
36 | ],
37 | "redis_use_cases": [],
38 | "redis_features": [
39 | "JSON"
40 | ],
41 | "app_image_urls": [
42 | "https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/main/preview.png?raw=true"
43 | ],
44 | "youtube_url": "https://www.youtube.com/watch?v=Fvjn5fuUxOU",
45 | "special_tags": [],
46 | "verticals": [
47 | "Retail"
48 | ],
49 | "markdown": "https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/main/README.md"
50 | }
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/preview.png
--------------------------------------------------------------------------------
/server/.env.example:
--------------------------------------------------------------------------------
1 | PORT=3000
2 |
3 | # Host and a port. Can be with `redis://` or without.
4 | # Host and a port encoded in redis uri take precedence over other environment variable.
5 | # preferable
6 | REDIS_ENDPOINT_URI=redis://127.0.0.1:6379
7 |
8 | # Or you can set it here (ie. for docker development)
9 | REDIS_HOST=127.0.0.1
10 | REDIS_PORT=6379
11 |
12 | # You can set password here
13 | REDIS_PASSWORD=
14 |
15 | COMPOSE_PROJECT_NAME=redis-shopping-cart
16 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | .env
4 | **/.DS_Store
5 | .idea/
6 |
--------------------------------------------------------------------------------
/server/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | tabWidth: 4,
4 | useTabs: false,
5 | semi: true,
6 | singleQuote: true,
7 | trailingComma: 'none',
8 | bracketSpacing: true,
9 | jsxBracketSameLine: false,
10 | proseWrap: 'never',
11 | htmlWhitespaceSensitivity: 'strict',
12 | endOfLine: 'lf',
13 | arrowParens: 'avoid'
14 | };
15 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Redis shopping cart API
2 |
3 | ## Prerequisites
4 |
5 | - Node - v12.19.0
6 | - NPM - v6.14.8
7 | - Docker - v19.03.13 (optional)
8 |
9 | ## Development
10 |
11 | ```
12 | # Environmental variables
13 |
14 | Copy `.env.example` to `.env` file and fill environmental variables
15 |
16 | - REDIS_PORT: Redis port (default: 6379)
17 | - REDIS_HOST: Redis host (default: 127.0.0.1)
18 | - REDIS_PASSWORD: Redis password (default: demo)
19 |
20 | cp .env.example .env
21 |
22 | # Run docker compose or install redis with RedisJson module manually. You can also go to https://redislabs.com/try-free/ and obtain necessary environmental variables
23 |
24 | docker network create global
25 | docker-compose up -d --build
26 |
27 | # Install dependencies
28 |
29 | npm install
30 |
31 | # Run dev server
32 |
33 | npm run dev
34 | ```
35 |
--------------------------------------------------------------------------------
/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | redis:
5 | image: redislabs/rejson:latest
6 | container_name: redis.redisshoppingcart.docker
7 | restart: unless-stopped
8 | environment:
9 | REDIS_PASSWORD: ${REDIS_PASSWORD}
10 | command: redis-server --loadmodule "/usr/lib/redis/modules/rejson.so" --requirepass "$REDIS_PASSWORD"
11 | ports:
12 | - 127.0.0.1:${REDIS_PORT}:6379
13 | networks:
14 | - global
15 | networks:
16 | global:
17 | external: true
18 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redis-shopping-cart-backend",
3 | "version": "1.0.0",
4 | "description": "This shopping cart is using Redis and RedisJson module functionalities, allowing to save JSON as keys using methods like json_get and json_set.",
5 | "keywords": [
6 | "NodeJS",
7 | "ExpressJS",
8 | "Redis",
9 | "Shopping Cart",
10 | "json_get",
11 | "json_set"
12 | ],
13 | "main": "src/index.js",
14 | "scripts": {
15 | "dev": "nodemon src/index.js",
16 | "test": "echo \"Error: no test specified\" && exit 1"
17 | },
18 | "author": "RemoteCraftsmen.com",
19 | "license": "MIT",
20 | "dependencies": {
21 | "body-parser": "^1.19.0",
22 | "connect-redis": "^5.0.0",
23 | "cors": "^2.8.5",
24 | "dotenv": "^8.2.0",
25 | "express": "^4.17.1",
26 | "express-session": "^1.17.1",
27 | "http-status-codes": "^2.1.4",
28 | "redis": "^3.0.2",
29 | "redis-rejson": "^1.0.0"
30 | },
31 | "devDependencies": {
32 | "nodemon": "^2.0.6"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/pm2.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "RedisShCApp",
5 | "script": "./src/index.js",
6 | "watch": false,
7 | "env_production": {
8 | "NODE_ENV": "production"
9 | },
10 | "exp_backoff_restart_delay": 250,
11 | "max_restarts": 10,
12 | "min_uptime": 5000,
13 | "max_memory_restart": "150M",
14 | "log_date_format": "YYYY-MM-DD HH:mm"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/server/src/controllers/Cart/DeleteItemController.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require('http-status-codes');
2 |
3 | class CartDeleteItemController {
4 | constructor(redisClientService) {
5 | this.redisClientService = redisClientService;
6 | }
7 |
8 | async index(req, res) {
9 | const { cartId } = req.session;
10 | const { id: productId } = req.params;
11 |
12 | const quantityInCart =
13 | parseInt(await this.redisClientService.hget(`cart:${cartId}`, `product:${productId}`)) || 0;
14 |
15 | if (quantityInCart) {
16 | await this.redisClientService.hdel(`cart:${cartId}`, `product:${productId}`);
17 |
18 | let productInStore = await this.redisClientService.jsonGet(`product:${productId}`);
19 |
20 | productInStore = JSON.parse(productInStore);
21 | productInStore.stock += quantityInCart;
22 |
23 | await this.redisClientService.jsonSet(`product:${productId}`, '.', JSON.stringify(productInStore));
24 | }
25 |
26 | return res.sendStatus(StatusCodes.NO_CONTENT);
27 | }
28 | }
29 |
30 | module.exports = CartDeleteItemController;
31 |
--------------------------------------------------------------------------------
/server/src/controllers/Cart/EmptyController.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require('http-status-codes');
2 |
3 | class CartEmptyController {
4 | constructor(redisClientService) {
5 | this.redisClientService = redisClientService;
6 | }
7 |
8 | async index(req, res) {
9 | const { cartId } = req.session;
10 |
11 | const cartList = await this.redisClientService.hgetall(`cart:${cartId}`);
12 |
13 | if (!cartList) {
14 | return res.sendStatus(StatusCodes.NO_CONTENT);
15 | }
16 |
17 | for (const key of Object.keys(cartList)) {
18 | await this.redisClientService.hdel(`cart:${cartId}`, key);
19 |
20 | let productInStore = await this.redisClientService.jsonGet(key);
21 |
22 | productInStore = JSON.parse(productInStore);
23 | productInStore.stock += parseInt(cartList[key]);
24 |
25 | await this.redisClientService.jsonSet(key, '.', JSON.stringify(productInStore));
26 | }
27 |
28 | return res.sendStatus(StatusCodes.NO_CONTENT);
29 | }
30 | }
31 |
32 | module.exports = CartEmptyController;
33 |
--------------------------------------------------------------------------------
/server/src/controllers/Cart/IndexController.js:
--------------------------------------------------------------------------------
1 | class CartIndexController {
2 | constructor(redisClientService) {
3 | this.redisClientService = redisClientService;
4 | }
5 |
6 | async index(req, res) {
7 | const { cartId } = req.session;
8 | let productList = [];
9 |
10 | const cartList = await this.redisClientService.hgetall(`cart:${cartId}`);
11 |
12 | if (!cartList) {
13 | return res.send(productList);
14 | }
15 |
16 | for (const itemKey of Object.keys(cartList)) {
17 | const product = await this.redisClientService.jsonGet(itemKey);
18 |
19 | productList.push({ product: JSON.parse(product), quantity: cartList[itemKey] });
20 | }
21 |
22 | return res.send(productList);
23 | }
24 | }
25 |
26 | module.exports = CartIndexController;
27 |
--------------------------------------------------------------------------------
/server/src/controllers/Cart/UpdateController.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require('http-status-codes');
2 |
3 | class CartUpdateController {
4 | constructor(redisClientService) {
5 | this.redisClientService = redisClientService;
6 | }
7 |
8 | async index(req, res) {
9 | const {
10 | session: { cartId },
11 | params: { id: productId }
12 | } = req;
13 |
14 | let { quantity, incrementBy } = req.body;
15 |
16 | let productInStore = await this.redisClientService.jsonGet(`product:${productId}`);
17 |
18 | if (!productInStore) {
19 | return res.status(StatusCodes.NOT_FOUND).send({ message: "Product with this id doesn't exist" });
20 | }
21 |
22 | let quantityInCart = (await this.redisClientService.hget(`cart:${cartId}`, `product:${productId}`)) || 0;
23 |
24 | quantityInCart = parseInt(quantityInCart);
25 |
26 | productInStore = JSON.parse(productInStore);
27 | const { stock } = productInStore;
28 |
29 | if (quantity) {
30 | quantity = parseInt(quantity);
31 |
32 | if (quantity <= 0) {
33 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Quantity should be greater than 0' });
34 | }
35 |
36 | const newStock = stock - (quantity - quantityInCart);
37 |
38 | if (newStock < 0) {
39 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Not enough products in stock' });
40 | }
41 |
42 | await this.redisClientService.hset(`cart:${cartId}`, `product:${productId}`, quantity);
43 |
44 | productInStore.stock = newStock;
45 |
46 | await this.redisClientService.jsonSet(`product:${productId}`, '.', JSON.stringify(productInStore));
47 | }
48 |
49 | if (incrementBy) {
50 | incrementBy = parseInt(incrementBy);
51 |
52 | if (incrementBy !== 1 && incrementBy !== -1) {
53 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Value of incrementBy should be 1 or -1' });
54 | }
55 |
56 | const quantityAfterIncrement = quantityInCart + incrementBy;
57 |
58 | if (quantityAfterIncrement <= 0 || stock - incrementBy < 0) {
59 | return res.status(StatusCodes.BAD_REQUEST).send({ message: "Can't decrement stock to 0" });
60 | }
61 |
62 | if (stock - incrementBy < 0) {
63 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Not enough products in stock' });
64 | }
65 |
66 | await this.redisClientService.hincrby(`cart:${cartId}`, `product:${productId}`, incrementBy);
67 |
68 | productInStore.stock -= incrementBy;
69 |
70 | await this.redisClientService.jsonSet(`product:${productId}`, '.', JSON.stringify(productInStore));
71 | }
72 |
73 | return res.sendStatus(StatusCodes.OK);
74 | }
75 | }
76 |
77 | module.exports = CartUpdateController;
78 |
--------------------------------------------------------------------------------
/server/src/controllers/Product/IndexController.js:
--------------------------------------------------------------------------------
1 | const { products } = require('../../products.json');
2 |
3 | class ProductIndexController {
4 | constructor(redisClientService) {
5 | this.redisClientService = redisClientService;
6 | }
7 |
8 | async index(req, res) {
9 | const productKeys = await this.redisClientService.scan('product:*');
10 | const productList = [];
11 |
12 | if (productKeys.length) {
13 | for (const key of productKeys) {
14 | const product = await this.redisClientService.jsonGet(key);
15 |
16 | productList.push(JSON.parse(product));
17 | }
18 |
19 | return res.send(productList);
20 | }
21 |
22 | for (const product of products) {
23 | const { id } = product;
24 |
25 | await this.redisClientService.jsonSet(`product:${id}`, '.', JSON.stringify(product));
26 |
27 | productList.push(product);
28 | }
29 |
30 | return res.send(productList);
31 | }
32 | }
33 |
34 | module.exports = ProductIndexController;
35 |
--------------------------------------------------------------------------------
/server/src/controllers/Product/ResetController.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require('http-status-codes');
2 | const { products } = require('../../products.json');
3 |
4 | class ProductResetController {
5 | constructor(redisClientService) {
6 | this.redisClientService = redisClientService;
7 | }
8 |
9 | async index(req, res) {
10 | const cartKeys = await this.redisClientService.scan('cart:*');
11 |
12 | for (const key of cartKeys) {
13 | await this.redisClientService.del(key);
14 | }
15 |
16 | for (const product of products) {
17 | const { id } = product;
18 |
19 | await this.redisClientService.jsonSet(`product:${id}`, '.', JSON.stringify(product));
20 | }
21 |
22 | return res.sendStatus(StatusCodes.OK);
23 | }
24 | }
25 |
26 | module.exports = ProductResetController;
27 |
--------------------------------------------------------------------------------
/server/src/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const redis = require('redis');
3 | const rejson = require('redis-rejson');
4 | const session = require('express-session');
5 | const RedisStore = require('connect-redis')(session);
6 | const path = require('path');
7 | const bodyParser = require('body-parser');
8 | const cors = require('cors');
9 | const RedisClient = require('./services/RedisClient');
10 |
11 | rejson(redis);
12 |
13 | require('dotenv').config();
14 |
15 | const { REDIS_ENDPOINT_URI, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, PORT } = process.env;
16 |
17 | const app = express();
18 |
19 | app.use(
20 | cors({
21 | origin(origin, callback) {
22 | callback(null, true);
23 | },
24 | credentials: true
25 | })
26 | );
27 |
28 | const redisEndpointUri = REDIS_ENDPOINT_URI
29 | ? REDIS_ENDPOINT_URI.replace(/^(redis\:\/\/)/, '')
30 | : `${REDIS_HOST}:${REDIS_PORT}`;
31 |
32 | const redisClient = redis.createClient(`redis://${redisEndpointUri}`, {
33 | password: REDIS_PASSWORD
34 | });
35 |
36 | const redisClientService = new RedisClient(redisClient);
37 |
38 | app.set('redisClientService', redisClientService);
39 |
40 | app.use(
41 | session({
42 | store: new RedisStore({ client: redisClient }),
43 | secret: 'someSecret',
44 | resave: false,
45 | saveUninitialized: false,
46 | rolling: true,
47 | cookie: {
48 | maxAge: 3600 * 1000 * 3
49 | }
50 | })
51 | );
52 |
53 | app.use(bodyParser.json());
54 |
55 | app.use('/', express.static(path.join(__dirname, '../../client-dist')));
56 |
57 | const router = require('./routes')(app);
58 |
59 | app.use('/api', router);
60 |
61 | const port = PORT || 3000;
62 |
63 | app.listen(port, () => {
64 | console.log(`App listening on port ${port}`);
65 | });
66 |
--------------------------------------------------------------------------------
/server/src/middleware/checkSession.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 |
3 | module.exports = (req, res, next) => {
4 | if (req.session && req.session.cartId) {
5 | return next();
6 | }
7 |
8 | req.session.cartId = crypto.randomBytes(16).toString('hex');
9 |
10 | return next();
11 | };
12 |
--------------------------------------------------------------------------------
/server/src/products.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": [
3 | {
4 | "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6",
5 | "name": "Brilliant Watch",
6 | "price": "250.00",
7 | "stock": 2
8 | },
9 | {
10 | "id": "f9a6d214-1c38-47ab-a61c-c99a59438b12",
11 | "name": "Old fashion cellphone",
12 | "price": "24.00",
13 | "stock": 2
14 | },
15 | {
16 | "id": "1f1321bb-0542-45d0-9601-2a3d007d5842",
17 | "name": "Modern iPhone",
18 | "price": "1000.00",
19 | "stock": 2
20 | },
21 | {
22 | "id": "f5384efc-eadb-4d7b-a131-36516269c218",
23 | "name": "Beautiful Sunglasses",
24 | "price": "12.00",
25 | "stock": 2
26 | },
27 | {
28 | "id": "6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345",
29 | "name": "Stylish Cup",
30 | "price": "8.00",
31 | "stock": 2
32 | },
33 | {
34 | "id": "efe0c7a3-9835-4dfb-87e1-575b7d06701a",
35 | "name": "Herb caps",
36 | "price": "12.00",
37 | "stock": 2
38 | },
39 | {
40 | "id": "x341115a-63d2-42ce-8fe0-5f696ecdfca6",
41 | "name": "Audiophile Headphones",
42 | "price": "550.00",
43 | "stock": 2
44 | },
45 | {
46 | "id": "42860491-9f15-43d4-adeb-0db2cc99174a",
47 | "name": "Digital Camera",
48 | "price": "225.00",
49 | "stock": 2
50 | },
51 | {
52 | "id": "63a3c635-4505-4588-8457-ed04fbb76511",
53 | "name": "Empty Bluray Disc",
54 | "price": "5.00",
55 | "stock": 2
56 | },
57 | {
58 | "id": "97a19842-db31-4537-9241-5053d7c96239",
59 | "name": "256BG Pendrive",
60 | "price": "60.00",
61 | "stock": 2
62 | }
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/server/src/routes/cart.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const checkSession = require('../middleware/checkSession');
4 | const IndexController = require('../controllers/Cart/IndexController');
5 | const UpdateController = require('../controllers/Cart/UpdateController');
6 | const DeleteItemController = require('../controllers/Cart/DeleteItemController');
7 | const EmptyController = require('../controllers/Cart/EmptyController');
8 |
9 | module.exports = app => {
10 | const redisClientService = app.get('redisClientService');
11 |
12 | const indexController = new IndexController(redisClientService);
13 | const updateController = new UpdateController(redisClientService);
14 | const deleteItemController = new DeleteItemController(redisClientService);
15 | const emptyController = new EmptyController(redisClientService);
16 |
17 | router.get('/', [checkSession], (...args) => indexController.index(...args));
18 | router.put('/:id', [checkSession], (...args) => updateController.index(...args));
19 | router.delete('/:id', [checkSession], (...args) => deleteItemController.index(...args));
20 | router.delete('/', [checkSession], (...args) => emptyController.index(...args));
21 |
22 | return router;
23 | };
24 |
--------------------------------------------------------------------------------
/server/src/routes/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const express = require('express');
3 | const router = express.Router();
4 |
5 | module.exports = app => {
6 | fs.readdirSync(__dirname).forEach(function (route) {
7 | route = route.split('.')[0];
8 |
9 | if (route === 'index') {
10 | return;
11 | }
12 |
13 | router.use(`/${route}`, require(`./${route}`)(app));
14 | });
15 |
16 | return router;
17 | };
18 |
--------------------------------------------------------------------------------
/server/src/routes/products.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const checkSession = require('../middleware/checkSession');
4 | const IndexController = require('../controllers/Product/IndexController');
5 | const ResetController = require('../controllers/Product/ResetController');
6 |
7 | module.exports = app => {
8 | const redisClientService = app.get('redisClientService');
9 |
10 | const indexController = new IndexController(redisClientService);
11 | const resetController = new ResetController(redisClientService);
12 |
13 | router.get('/', [checkSession], (...args) => indexController.index(...args));
14 | router.post('/reset', (...args) => resetController.index(...args));
15 |
16 | return router;
17 | };
18 |
--------------------------------------------------------------------------------
/server/src/services/RedisClient.js:
--------------------------------------------------------------------------------
1 | const { promisify } = require('util');
2 |
3 | class RedisClient {
4 | constructor(redisClient) {
5 | ['json_get', 'json_set', 'hgetall', 'hset', 'hget', 'hdel', 'hincrby', 'del', 'scan'].forEach(
6 | method => (redisClient[method] = promisify(redisClient[method]))
7 | );
8 | this.redis = redisClient;
9 | }
10 |
11 | async scan(pattern) {
12 | let matchingKeysCount = 0;
13 | let keys = [];
14 |
15 | const recursiveScan = async (cursor = '0') => {
16 | const [newCursor, matchingKeys] = await this.redis.scan(cursor, 'MATCH', pattern);
17 | cursor = newCursor;
18 |
19 | matchingKeysCount += matchingKeys.length;
20 | keys = keys.concat(matchingKeys);
21 |
22 | if (cursor === '0') {
23 | return keys;
24 | } else {
25 | return await recursiveScan(cursor);
26 | }
27 | };
28 |
29 | return await recursiveScan();
30 | }
31 |
32 | jsonGet(key) {
33 | return this.redis.json_get(key);
34 | }
35 |
36 | jsonSet(key, path, json) {
37 | return this.redis.json_set(key, path, json);
38 | }
39 |
40 | hgetall(key) {
41 | return this.redis.hgetall(key);
42 | }
43 |
44 | hset(hash, key, value) {
45 | return this.redis.hset(hash, key, value);
46 | }
47 |
48 | hget(hash, key) {
49 | return this.redis.hget(hash, key);
50 | }
51 |
52 | hdel(hash, key) {
53 | return this.redis.hdel(hash, key);
54 | }
55 |
56 | hincrby(hash, key, incr) {
57 | return this.redis.hincrby(hash, key, incr);
58 | }
59 |
60 | del(key) {
61 | return this.redis.del(key);
62 | }
63 | }
64 |
65 | module.exports = RedisClient;
66 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [{ "src": "server/src/index.js", "use": "@now/node" }],
4 | "routes": [{ "src": "(.*)", "dest": "server/src/index.js" }]
5 | }
6 |
--------------------------------------------------------------------------------