├── .gitignore ├── README.md ├── db.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── components └── ComponentToPrint.jsx ├── index.css ├── index.js ├── layouts └── MainLayout.jsx └── pages ├── HomePage.jsx └── POSPage.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Tutorial - Create a Point of Sale System for Beginners in 1 Hour (React POS) 2 | 3 | This project was built for teaching React for beginners in 1 Hour. Students learn how to set up a real world react project and build a Point of Sale System. The video for this tutorial is available on Devtamin's Youtube Channel, [https://www.youtube.com/watch?v=8E7Xwy0lXlg](https://www.youtube.com/watch?v=8E7Xwy0lXlg). Please, enjoy the video and subscribe Devtamin on Youtube. 4 | 5 | ## How to set up the project 6 | 7 | - Clone this respository to your computer 8 | - Access to the project folder on the computer via Termaial or Git Bash 9 | - Excute `npm install` to download all necessary packages 10 | - Excute `npm start` to start the project 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": 123, 5 | "name": "orange", 6 | "price": "2", 7 | "image": "https://cdn.pixabay.com/photo/2017/01/20/15/06/oranges-1995056_960_720.jpg" 8 | }, 9 | { 10 | "id": 131, 11 | "name": "Milk", 12 | "price": "3", 13 | "image": "https://cdn.pixabay.com/photo/2018/03/16/16/42/milk-3231772_960_720.jpg" 14 | }, 15 | { 16 | "id": 132, 17 | "name": "Ice Cream", 18 | "price": "4", 19 | "image": "https://cdn.pixabay.com/photo/2016/12/26/16/09/bowl-1932375_960_720.jpg" 20 | }, 21 | { 22 | "id": 133, 23 | "name": "Salmon", 24 | "price": "10", 25 | "image": "https://cdn.pixabay.com/photo/2021/01/05/23/18/salmon-5892659_960_720.jpg" 26 | }, 27 | { 28 | "id": 134, 29 | "name": "Watermelon", 30 | "price": "2", 31 | "image": "https://cdn.pixabay.com/photo/2015/09/27/18/18/watermelons-961128_960_720.jpg" 32 | }, 33 | { 34 | "id": 135, 35 | "name": "Potato", 36 | "price": "4", 37 | "image": "https://cdn.pixabay.com/photo/2016/08/11/08/43/potatoes-1585060_960_720.jpg" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.1.1", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^0.27.2", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-router-dom": "^6.3.0", 13 | "react-scripts": "5.0.1", 14 | "react-to-print": "^2.14.7", 15 | "react-toastify": "^8.2.0", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "proxy": "http://localhost:5000" 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phithounsavanh/react-pos/2f5be155f85852cefdfb06b18534b8095a688337/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | React App 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phithounsavanh/react-pos/2f5be155f85852cefdfb06b18534b8095a688337/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phithounsavanh/react-pos/2f5be155f85852cefdfb06b18534b8095a688337/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserRouter as Router, 3 | Routes, 4 | Route, 5 | } from 'react-router-dom'; 6 | import HomePage from './pages/HomePage'; 7 | import POSPage from './pages/POSPage'; 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 | } /> 14 | } /> 15 | 16 | 17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/components/ComponentToPrint.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ComponentToPrint = React.forwardRef((props, ref) => { 4 | const {cart, totalAmount} = props; 5 | return ( 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | { cart ? cart.map((cartProduct, key) => 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | : ''} 29 | 30 |
#NamePriceQtyTotal
{cartProduct.id}{cartProduct.name}{cartProduct.price}{cartProduct.quantity}{cartProduct.totalAmount}
31 |

Total Amount: ${totalAmount}

32 |
33 | ); 34 | }); -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .pos-item{ 16 | cursor: pointer; 17 | font-weight: bold; 18 | } 19 | 20 | .pos-item:hover{ 21 | background: rgb(2, 85, 162); 22 | color: white; 23 | } 24 | 25 | .pos-item img{ 26 | width: 100%; 27 | max-height: 100px; 28 | object-fit: cover; 29 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | ); 10 | 11 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router-dom' 3 | import { ToastContainer } from 'react-toastify'; 4 | import 'react-toastify/dist/ReactToastify.css'; 5 | 6 | function MainLayout({children}) { 7 | return ( 8 |
9 |
10 | 15 |
16 |
17 |
18 | {children} 19 |
20 | 21 |
22 |
23 | ) 24 | } 25 | 26 | export default MainLayout -------------------------------------------------------------------------------- /src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router-dom' 3 | import MainLayout from '../layouts/MainLayout' 4 | 5 | function HomePage() { 6 | return ( 7 | 8 |
9 |

Welcome to the simple POS for small buiness

10 |

Labore tempor ipsum duis ea exercitation laboris laborum mollit qui exercitation.

11 |

If you have an issue, call 443-444-xxxx anytimes

12 | Click here to sell products 13 |
14 |
15 | ) 16 | } 17 | 18 | export default HomePage -------------------------------------------------------------------------------- /src/pages/POSPage.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react' 2 | import MainLayout from '../layouts/MainLayout' 3 | import axios from "axios" 4 | import { toast } from 'react-toastify'; 5 | import { ComponentToPrint } from '../components/ComponentToPrint'; 6 | import { useReactToPrint } from 'react-to-print'; 7 | 8 | function POSPage() { 9 | 10 | const [products, setProducts] = useState([]); 11 | const [isLoading, setIsLoading] = useState(false); 12 | const [cart, setCart] = useState([]); 13 | const [totalAmount, setTotalAmount] = useState(0); 14 | 15 | const toastOptions = { 16 | autoClose: 400, 17 | pauseOnHover: true, 18 | } 19 | 20 | const fetchProducts = async() => { 21 | setIsLoading(true); 22 | const result = await axios.get('products'); 23 | setProducts(await result.data); 24 | setIsLoading(false); 25 | } 26 | 27 | const addProductToCart = async(product) =>{ 28 | // check if the adding product exist 29 | let findProductInCart = await cart.find(i=>{ 30 | return i.id === product.id 31 | }); 32 | 33 | if(findProductInCart){ 34 | let newCart = []; 35 | let newItem; 36 | 37 | cart.forEach(cartItem => { 38 | if(cartItem.id === product.id){ 39 | newItem = { 40 | ...cartItem, 41 | quantity: cartItem.quantity + 1, 42 | totalAmount: cartItem.price * (cartItem.quantity + 1) 43 | } 44 | newCart.push(newItem); 45 | }else{ 46 | newCart.push(cartItem); 47 | } 48 | }); 49 | 50 | setCart(newCart); 51 | toast(`Added ${newItem.name} to cart`,toastOptions) 52 | 53 | }else{ 54 | let addingProduct = { 55 | ...product, 56 | 'quantity': 1, 57 | 'totalAmount': product.price, 58 | } 59 | setCart([...cart, addingProduct]); 60 | toast(`Added ${product.name} to cart`, toastOptions) 61 | } 62 | 63 | } 64 | 65 | const removeProduct = async(product) =>{ 66 | const newCart =cart.filter(cartItem => cartItem.id !== product.id); 67 | setCart(newCart); 68 | } 69 | 70 | const componentRef = useRef(); 71 | 72 | const handleReactToPrint = useReactToPrint({ 73 | content: () => componentRef.current, 74 | }); 75 | 76 | const handlePrint = () => { 77 | handleReactToPrint(); 78 | } 79 | 80 | useEffect(() => { 81 | fetchProducts(); 82 | },[]); 83 | 84 | useEffect(() => { 85 | let newTotalAmount = 0; 86 | cart.forEach(icart => { 87 | newTotalAmount = newTotalAmount + parseInt(icart.totalAmount); 88 | }) 89 | setTotalAmount(newTotalAmount); 90 | },[cart]) 91 | 92 | 93 | 94 | return ( 95 | 96 |
97 |
98 | {isLoading ? 'Loading' :
99 | {products.map((product, key) => 100 |
101 |
addProductToCart(product)}> 102 |

{product.name}

103 | {product.name} 104 |

${product.price}

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 | { cart ? cart.map((cartProduct, key) => 130 | 131 | 132 | 133 | 134 | 135 | 138 | 139 | ) 140 | 141 | : 'No Item in Cart'} 142 | 143 |
#NamePriceQtyTotalAction
{cartProduct.id}{cartProduct.name}{cartProduct.price}{cartProduct.quantity}{cartProduct.totalAmount} 136 | 137 |
144 |

Total Amount: ${totalAmount}

145 |
146 | 147 |
148 | { totalAmount !== 0 ?
149 | 152 | 153 |
: 'Please add a product to the cart' 154 | 155 | } 156 |
157 | 158 | 159 |
160 |
161 |
162 | ) 163 | } 164 | 165 | export default POSPage --------------------------------------------------------------------------------