diff --git a/package-lock.json b/package-lock.json index 74fe954..f954127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-dropdown-select": "^4.12.2", + "react-qr-barcode-scanner": "^2.1.8", "react-router-dom": "^7.6.3", "sweetalert2": "^11.22.2", "validator": "^13.15.15", @@ -49726,6 +49727,28 @@ "node": ">=16.0.0" } }, + "node_modules/@zxing/library": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz", + "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==", + "license": "MIT", + "dependencies": { + "ts-custom-error": "^3.2.1" + }, + "engines": { + "node": ">= 10.4.0" + }, + "optionalDependencies": { + "@zxing/text-encoding": "~0.9.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -56495,6 +56518,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-qr-barcode-scanner": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/react-qr-barcode-scanner/-/react-qr-barcode-scanner-2.1.8.tgz", + "integrity": "sha512-REbb+BWmPjvYIAZi6/iYrRupnK4e8d39m1YExhXk+XkKnz/aU/JembC1s+6ZO6jGVXnsGkh25Dwctcrm1HsK4g==", + "license": "MIT", + "dependencies": { + "@zxing/library": "^0.21.3", + "react-webcam": "^7.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -56543,6 +56580,16 @@ "react-dom": ">=18" } }, + "node_modules/react-webcam": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz", + "integrity": "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.2.0", + "react-dom": ">=16.2.0" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -58204,6 +58251,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-dedent": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-1.2.0.tgz", diff --git a/package.json b/package.json index 74e17c5..529de57 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-dropdown-select": "^4.12.2", + "react-qr-barcode-scanner": "^2.1.8", "react-router-dom": "^7.6.3", "sweetalert2": "^11.22.2", "validator": "^13.15.15", diff --git a/src/components/Scanner.tsx b/src/components/Scanner.tsx new file mode 100644 index 0000000..430465e --- /dev/null +++ b/src/components/Scanner.tsx @@ -0,0 +1,57 @@ +import React, { useState } from "react"; +import BarcodeScanner from "react-qr-barcode-scanner"; + +interface ScannerProps { + value: string; + onChange: (value: string) => void; +} + + +const Scanner: React.FC = ({ value, onChange }) => { + const [isScanning, setIsScanning] = useState(false); + const [loading, setLoading] = useState(false); + + return ( +
+ onChange(e.target.value)} + placeholder="Barcode" + autoComplete="off" + maxLength={200} + /> + + + + {isScanning && ( +
+
+ {loading &&
Loading barcode scanner...
} + { + if (loading) setLoading(false); + console.log(result); + if (result) { + const code = result.getText(); + onChange(code); + setIsScanning(false); + } + }} + /> + +
+
+ )} +
+ ); +}; + +export default Scanner; diff --git a/src/index.css b/src/index.css index aa6b374..8c2d05f 100644 --- a/src/index.css +++ b/src/index.css @@ -297,10 +297,12 @@ h1 { background-color: #2c2c2c; color: #f0f0f0; width: 100%; + box-sizing: border-box; } .add-form textarea { height: 10rem; + resize: none; } .add-form input[type="checkbox"] { @@ -321,10 +323,22 @@ h1 { margin-top: 1.5rem; } -.add-form .discount-group .big-chain-group { +.add-form .discount-group, .big-chain-group { display: contents; } +.discount-group label { + grid-column: 1; + align-self: center; +} + +.discount-group input[type="checkbox"] { + grid-column: 2; + justify-self: start; + align-self: center; + transform: scale(1.6); +} + .store-locations-group { grid-column: span 2; display: flex; @@ -435,6 +449,67 @@ h1 { font-size: 1.15rem; } +.barcode-scanner { + display: flex; + gap: 0.5em; +} + +.barcode-scanner-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.barcode-scanner-camera { + background: #fff; + border-radius: 12px; + padding: 1rem; + position: relative; + max-width: 500px; + width: 90%; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + height: 420px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 1em; +} + +.barcode-scanner-camera video { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; + z-index: 2; +} + +.barcode-scanner-camera .button { + background-color: #E85660; +} + +.barcode-scanner-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 1.2rem; + color: #555; + z-index: 1; + pointer-events: none; + width: 90%; + text-align: center; +} + + + @media (prefers-color-scheme: light) { :root { @@ -559,11 +634,10 @@ h1 { box-sizing: border-box; } - .add-form .discount-group .big-chain-group { + .add-form .discount-group, .big-chain-group { display: flex; align-items: center; gap: 0.75rem; - grid-column: 1 / -1; } .store-locations-group { @@ -574,11 +648,11 @@ h1 { grid-column: span 1; } - .add-form .discount-group .big-chain-group label { + .add-form .discount-group, .big-chain-group label { margin: 0; } - .add-form .discount-group .big-chain-group [type="checkbox"] { + .add-form .big-chain-group [type="checkbox"] { transform: scale(1.4); } } diff --git a/src/pages/CreateNewItem.tsx b/src/pages/CreateNewItem.tsx index d762e15..6cc4e4c 100644 --- a/src/pages/CreateNewItem.tsx +++ b/src/pages/CreateNewItem.tsx @@ -8,6 +8,7 @@ import UnitsDropdownSelect from "../custom-dropdown-select/UnitsDropdownSelect"; import StoresDropdownSelect from "../custom-dropdown-select/StoresDropdownSelect"; import { generateClient } from "aws-amplify/data"; import type { Schema } from "../../amplify/data/resource"; +import Scanner from "../components/Scanner"; const client = generateClient(); @@ -169,17 +170,16 @@ const CreateNewItem: React.FC = () => { onSubmit={handleSubmit} method="POST" > - - + + + + handleChange({ + target: { name: "barcode", value: val }, + } as React.ChangeEvent) + } + />