fix: qr scanner to use jsQR and add camera selector
This commit is contained in:
parent
1e0132990a
commit
9f8de81610
6 changed files with 188 additions and 96 deletions
|
|
@ -14,8 +14,8 @@
|
|||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@prisma/client": "^6.5.0",
|
||||
"@trafficlunar/asmcrypto.js": "^1.0.2",
|
||||
"@yudiel/react-qr-scanner": "2.2.2-beta.2",
|
||||
"bit-buffer": "^0.2.5",
|
||||
"downshift": "^9.0.9",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"jsqr": "^1.4.0",
|
||||
"next": "15.2.4",
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-select": "^5.10.1",
|
||||
"react-webcam": "^7.2.0",
|
||||
"sharp": "^0.34.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
|
|
|||
102
pnpm-lock.yaml
102
pnpm-lock.yaml
|
|
@ -20,12 +20,12 @@ importers:
|
|||
'@trafficlunar/asmcrypto.js':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
'@yudiel/react-qr-scanner':
|
||||
specifier: 2.2.2-beta.2
|
||||
version: 2.2.2-beta.2(@types/emscripten@1.40.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
bit-buffer:
|
||||
specifier: ^0.2.5
|
||||
version: 0.2.5
|
||||
downshift:
|
||||
specifier: ^9.0.9
|
||||
version: 9.0.9(react@19.1.0)
|
||||
embla-carousel-react:
|
||||
specifier: ^8.5.2
|
||||
version: 8.5.2(react@19.1.0)
|
||||
|
|
@ -53,6 +53,9 @@ importers:
|
|||
react-select:
|
||||
specifier: ^5.10.1
|
||||
version: 5.10.1(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-webcam:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
sharp:
|
||||
specifier: ^0.34.0
|
||||
version: 0.34.0
|
||||
|
|
@ -881,9 +884,6 @@ packages:
|
|||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
'@types/emscripten@1.40.1':
|
||||
resolution: {integrity: sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==}
|
||||
|
||||
'@types/estree@1.0.7':
|
||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||
|
||||
|
|
@ -1037,12 +1037,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@yudiel/react-qr-scanner@2.2.2-beta.2':
|
||||
resolution: {integrity: sha512-WrnO1ejWKItHjJSsWkR4wSP1hhCV3K1wjlaTUI6RdNyOzZnj5CV3mG5F8SQj/pGdkkxr2q7/qfIgqlB/arB4pg==}
|
||||
peerDependencies:
|
||||
react: ^17 || ^18 || ^19
|
||||
react-dom: ^17 || ^18 || ^19
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -1129,9 +1123,6 @@ packages:
|
|||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
barcode-detector@3.0.1:
|
||||
resolution: {integrity: sha512-3fCzG/Py4SVgZJhubD1mt7rVprtHEVWrxQN4FUOG0oulPE4193evbgyptxcOYsfTNEtMlWc+Ec9tdxhjlR4/Ww==}
|
||||
|
||||
bit-buffer@0.2.5:
|
||||
resolution: {integrity: sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA==}
|
||||
|
||||
|
|
@ -1189,6 +1180,9 @@ packages:
|
|||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
compute-scroll-into-view@3.1.1:
|
||||
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
|
|
@ -1267,6 +1261,11 @@ packages:
|
|||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
downshift@9.0.9:
|
||||
resolution: {integrity: sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==}
|
||||
peerDependencies:
|
||||
react: '>=16.12.0'
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2094,6 +2093,9 @@ packages:
|
|||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
|
||||
react-redux@9.2.0:
|
||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||
peerDependencies:
|
||||
|
|
@ -2118,6 +2120,12 @@ packages:
|
|||
react: '>=16.6.0'
|
||||
react-dom: '>=16.6.0'
|
||||
|
||||
react-webcam@7.2.0:
|
||||
resolution: {integrity: sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==}
|
||||
peerDependencies:
|
||||
react: '>=16.2.0'
|
||||
react-dom: '>=16.2.0'
|
||||
|
||||
react@19.1.0:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2174,9 +2182,6 @@ packages:
|
|||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
sdp@3.2.0:
|
||||
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
|
@ -2337,10 +2342,6 @@ packages:
|
|||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
type-fest@4.39.1:
|
||||
resolution: {integrity: sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2389,10 +2390,6 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
webrtc-adapter@9.0.1:
|
||||
resolution: {integrity: sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==}
|
||||
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2429,11 +2426,6 @@ packages:
|
|||
zod@3.24.2:
|
||||
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
||||
|
||||
zxing-wasm@2.1.0:
|
||||
resolution: {integrity: sha512-CvuwDZHRHwg6PeCARaCDIp3dauD4cin0mbHrQQZtMDrr5mblPzCimAjdw/XMhD/Au10q/f5+SAupvYqYvUOg1Q==}
|
||||
peerDependencies:
|
||||
'@types/emscripten': '>=1.39.6'
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
|
@ -3096,8 +3088,6 @@ snapshots:
|
|||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/emscripten@1.40.1': {}
|
||||
|
||||
'@types/estree@1.0.7': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
|
@ -3248,15 +3238,6 @@ snapshots:
|
|||
'@unrs/resolver-binding-win32-x64-msvc@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@yudiel/react-qr-scanner@2.2.2-beta.2(@types/emscripten@1.40.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
barcode-detector: 3.0.1(@types/emscripten@1.40.1)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
webrtc-adapter: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/emscripten'
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.14.1):
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
|
|
@ -3365,12 +3346,6 @@ snapshots:
|
|||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
barcode-detector@3.0.1(@types/emscripten@1.40.1):
|
||||
dependencies:
|
||||
zxing-wasm: 2.1.0(@types/emscripten@1.40.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/emscripten'
|
||||
|
||||
bit-buffer@0.2.5: {}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
|
|
@ -3434,6 +3409,8 @@ snapshots:
|
|||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
|
||||
compute-scroll-into-view@3.1.1: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
convert-source-map@1.9.0: {}
|
||||
|
|
@ -3513,6 +3490,15 @@ snapshots:
|
|||
'@babel/runtime': 7.27.0
|
||||
csstype: 3.1.3
|
||||
|
||||
downshift@9.0.9(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
compute-scroll-into-view: 3.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
react-is: 18.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
|
|
@ -4486,6 +4472,8 @@ snapshots:
|
|||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@18.2.0: {}
|
||||
|
||||
react-redux@9.2.0(@types/react@19.1.0)(react@19.1.0)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
|
|
@ -4521,6 +4509,11 @@ snapshots:
|
|||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-webcam@7.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react@19.1.0: {}
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
|
@ -4590,8 +4583,6 @@ snapshots:
|
|||
|
||||
scheduler@0.26.0: {}
|
||||
|
||||
sdp@3.2.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.1: {}
|
||||
|
|
@ -4817,8 +4808,6 @@ snapshots:
|
|||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
type-fest@4.39.1: {}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
|
@ -4895,10 +4884,6 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
webrtc-adapter@9.0.1:
|
||||
dependencies:
|
||||
sdp: 3.2.0
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
|
|
@ -4951,8 +4936,3 @@ snapshots:
|
|||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@3.24.2: {}
|
||||
|
||||
zxing-wasm@2.1.0(@types/emscripten@1.40.1):
|
||||
dependencies:
|
||||
'@types/emscripten': 1.40.1
|
||||
type-fest: 4.39.1
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export default function SubmitForm() {
|
|||
|
||||
<span>or</span>
|
||||
|
||||
<button onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2">
|
||||
<button type="button" onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2">
|
||||
<Icon icon="mdi:camera" fontSize={20} />
|
||||
Use your camera
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Webcam from "react-webcam";
|
||||
import jsQR from "jsqr";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import QrFinder from "../qr-finder";
|
||||
import QrFinder from "./qr-finder";
|
||||
import { useSelect } from "downshift";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
|
|
@ -15,62 +17,164 @@ interface Props {
|
|||
export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||
const [permissionGranted, setPermissionGranted] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
||||
|
||||
const webcamRef = useRef<Webcam>(null);
|
||||
const requestRef = useRef<number>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const cameraItems = devices.map((device) => ({
|
||||
value: device.deviceId,
|
||||
label: device.label || `Camera ${device.deviceId.slice(-5)}`,
|
||||
}));
|
||||
|
||||
const {
|
||||
isOpen: isDropdownOpen,
|
||||
getToggleButtonProps,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
selectedItem,
|
||||
} = useSelect({
|
||||
items: cameraItems,
|
||||
selectedItem: cameraItems.find((item) => item.value === selectedDeviceId) ?? null,
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
setSelectedDeviceId(selectedItem?.value ?? null);
|
||||
},
|
||||
});
|
||||
|
||||
const scanQRCode = useCallback(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
// Continue scanning in a loop
|
||||
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||
|
||||
const webcam = webcamRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!webcam || !canvas) return;
|
||||
|
||||
const video = webcam.video;
|
||||
if (!video || video.videoWidth === 0 || video.videoHeight === 0) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, video.videoWidth, video.videoHeight);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
if (!code) return;
|
||||
|
||||
console.log(code);
|
||||
|
||||
// Cancel animation frame to stop scanning
|
||||
if (requestRef.current) {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
}
|
||||
|
||||
setQrBytesRaw(code.binaryData!);
|
||||
setIsOpen(false);
|
||||
}, [isOpen, setIsOpen, setQrBytesRaw]);
|
||||
|
||||
const requestPermission = async () => {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true })
|
||||
.then(() => setPermissionGranted(true))
|
||||
.catch(() => setPermissionGranted(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
requestPermission();
|
||||
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
const videoDevices = devices.filter((d) => d.kind === "videoinput");
|
||||
setDevices(videoDevices);
|
||||
if (!selectedDeviceId && videoDevices.length > 0) {
|
||||
setSelectedDeviceId(videoDevices[0].deviceId);
|
||||
}
|
||||
});
|
||||
}, [isOpen]);
|
||||
|
||||
const handleScan = (result: IDetectedBarcode[]) => {
|
||||
// todo: fix scan, use jsQR instead, data is wrong
|
||||
setIsOpen(false);
|
||||
|
||||
// Convert to bytes
|
||||
// const encoder = new TextEncoder();
|
||||
// const byteArray = encoder.encode(result[0].rawValue);
|
||||
|
||||
// setQrBytes(byteArray);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isOpen && !permissionGranted) return;
|
||||
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||
}, [permissionGranted]);
|
||||
|
||||
if (isOpen)
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center z-40 backdrop-brightness-75 backdrop-blur-xs">
|
||||
<div className="bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-xl font-bold">Scan QR Code</h2>
|
||||
<button onClick={() => setIsOpen(false)} className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
||||
<Icon icon="material-symbols:close-rounded" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{devices.length > 0 && (
|
||||
<div className="mb-4 flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold">Camera:</label>
|
||||
<div className="relative w-full">
|
||||
{/* Toggle button to open the dropdown */}
|
||||
<button type="button" {...getToggleButtonProps()} className="pill input w-full !px-2 !py-0.5 !justify-between text-sm">
|
||||
{selectedItem?.label || "Select a camera"}
|
||||
|
||||
<Icon icon="tabler:chevron-down" className="ml-2 size-5" />
|
||||
</button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
<ul
|
||||
{...getMenuProps()}
|
||||
className={`absolute z-50 w-full bg-orange-200 border-2 border-orange-400 rounded-lg mt-1 shadow-lg max-h-60 overflow-y-auto ${
|
||||
isDropdownOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{isDropdownOpen &&
|
||||
cameraItems.map((item, index) => (
|
||||
<li
|
||||
key={item.value}
|
||||
{...getItemProps({ item, index })}
|
||||
className={`px-4 py-1 cursor-pointer text-sm ${highlightedIndex === index ? "bg-black/15" : ""}`}
|
||||
>
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative w-full aspect-square">
|
||||
{permissionGranted === null ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg border-2 border-amber-500">
|
||||
<div className="text-center p-4">
|
||||
<p className="text-red-400 font-bold text-lg mb-2">Camera access denied</p>
|
||||
<p className="text-gray-600">Please allow camera access in your browser settings to scan QR codes</p>
|
||||
</div>
|
||||
{!permissionGranted ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center rounded-2xl border-2 border-amber-500 text-center p-8">
|
||||
<p className="text-red-400 font-bold text-lg mb-2">Camera access denied</p>
|
||||
<p className="text-gray-600">Please allow camera access in your browser settings to scan QR codes</p>
|
||||
<button type="button" onClick={requestPermission} className="pill button text-xs mt-2 !py-0.5 !px-2">
|
||||
Request Permission
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Scanner
|
||||
formats={["qr_code"]}
|
||||
onScan={handleScan}
|
||||
components={{ finder: false }}
|
||||
sound={false}
|
||||
classNames={{ container: "rounded-lg border-2 border-amber-500" }}
|
||||
<Webcam
|
||||
ref={webcamRef}
|
||||
audio={false}
|
||||
videoConstraints={{
|
||||
deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined,
|
||||
}}
|
||||
className="size-full object-cover rounded-2xl border-2 border-amber-500"
|
||||
/>
|
||||
|
||||
<QrFinder />
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button onClick={() => setIsOpen(false)} className="pill button">
|
||||
<button type="button" onClick={() => setIsOpen(false)} className="pill button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { FileWithPath, useDropzone } from "react-dropzone";
|
||||
import { Icon } from "@iconify/react";
|
||||
import jsQR from "jsqr";
|
||||
|
|
@ -10,6 +10,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
|
||||
acceptedFiles.forEach((file) => {
|
||||
// Scan QR code
|
||||
|
|
@ -17,7 +19,9 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
|||
reader.onload = async (event) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
|
|
@ -26,9 +30,10 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
|||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
||||
const decoded = jsQR(imageData.data, image.width, image.height);
|
||||
const code = jsQR(imageData.data, image.width, image.height);
|
||||
if (!code) return;
|
||||
|
||||
setQrBytesRaw(decoded?.binaryData!);
|
||||
setQrBytesRaw(code.binaryData!);
|
||||
};
|
||||
image.src = event.target!.result as string;
|
||||
};
|
||||
|
|
@ -59,6 +64,8 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
|||
or click to open
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue