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",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
"@trafficlunar/asmcrypto.js": "^1.0.2",
|
"@trafficlunar/asmcrypto.js": "^1.0.2",
|
||||||
"@yudiel/react-qr-scanner": "2.2.2-beta.2",
|
|
||||||
"bit-buffer": "^0.2.5",
|
"bit-buffer": "^0.2.5",
|
||||||
|
"downshift": "^9.0.9",
|
||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.5.2",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-select": "^5.10.1",
|
"react-select": "^5.10.1",
|
||||||
|
"react-webcam": "^7.2.0",
|
||||||
"sharp": "^0.34.0",
|
"sharp": "^0.34.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
102
pnpm-lock.yaml
102
pnpm-lock.yaml
|
|
@ -20,12 +20,12 @@ importers:
|
||||||
'@trafficlunar/asmcrypto.js':
|
'@trafficlunar/asmcrypto.js':
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 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:
|
bit-buffer:
|
||||||
specifier: ^0.2.5
|
specifier: ^0.2.5
|
||||||
version: 0.2.5
|
version: 0.2.5
|
||||||
|
downshift:
|
||||||
|
specifier: ^9.0.9
|
||||||
|
version: 9.0.9(react@19.1.0)
|
||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.5.2
|
specifier: ^8.5.2
|
||||||
version: 8.5.2(react@19.1.0)
|
version: 8.5.2(react@19.1.0)
|
||||||
|
|
@ -53,6 +53,9 @@ importers:
|
||||||
react-select:
|
react-select:
|
||||||
specifier: ^5.10.1
|
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)
|
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:
|
sharp:
|
||||||
specifier: ^0.34.0
|
specifier: ^0.34.0
|
||||||
version: 0.34.0
|
version: 0.34.0
|
||||||
|
|
@ -881,9 +884,6 @@ packages:
|
||||||
'@types/cookie@0.6.0':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
'@types/emscripten@1.40.1':
|
|
||||||
resolution: {integrity: sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==}
|
|
||||||
|
|
||||||
'@types/estree@1.0.7':
|
'@types/estree@1.0.7':
|
||||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||||
|
|
||||||
|
|
@ -1037,12 +1037,6 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
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:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1129,9 +1123,6 @@ packages:
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
barcode-detector@3.0.1:
|
|
||||||
resolution: {integrity: sha512-3fCzG/Py4SVgZJhubD1mt7rVprtHEVWrxQN4FUOG0oulPE4193evbgyptxcOYsfTNEtMlWc+Ec9tdxhjlR4/Ww==}
|
|
||||||
|
|
||||||
bit-buffer@0.2.5:
|
bit-buffer@0.2.5:
|
||||||
resolution: {integrity: sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA==}
|
resolution: {integrity: sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA==}
|
||||||
|
|
||||||
|
|
@ -1189,6 +1180,9 @@ packages:
|
||||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||||
engines: {node: '>=12.5.0'}
|
engines: {node: '>=12.5.0'}
|
||||||
|
|
||||||
|
compute-scroll-into-view@3.1.1:
|
||||||
|
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
|
@ -1267,6 +1261,11 @@ packages:
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
|
downshift@9.0.9:
|
||||||
|
resolution: {integrity: sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.12.0'
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2094,6 +2093,9 @@ packages:
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
react-is@18.2.0:
|
||||||
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
|
|
||||||
react-redux@9.2.0:
|
react-redux@9.2.0:
|
||||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -2118,6 +2120,12 @@ packages:
|
||||||
react: '>=16.6.0'
|
react: '>=16.6.0'
|
||||||
react-dom: '>=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:
|
react@19.1.0:
|
||||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -2174,9 +2182,6 @@ packages:
|
||||||
scheduler@0.26.0:
|
scheduler@0.26.0:
|
||||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||||
|
|
||||||
sdp@3.2.0:
|
|
||||||
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
|
|
||||||
|
|
||||||
semver@6.3.1:
|
semver@6.3.1:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -2337,10 +2342,6 @@ packages:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
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:
|
typed-array-buffer@1.0.3:
|
||||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2389,10 +2390,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
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:
|
which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2429,11 +2426,6 @@ packages:
|
||||||
zod@3.24.2:
|
zod@3.24.2:
|
||||||
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
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:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
@ -3096,8 +3088,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
'@types/emscripten@1.40.1': {}
|
|
||||||
|
|
||||||
'@types/estree@1.0.7': {}
|
'@types/estree@1.0.7': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
@ -3248,15 +3238,6 @@ snapshots:
|
||||||
'@unrs/resolver-binding-win32-x64-msvc@1.3.3':
|
'@unrs/resolver-binding-win32-x64-msvc@1.3.3':
|
||||||
optional: true
|
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):
|
acorn-jsx@5.3.2(acorn@8.14.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.14.1
|
acorn: 8.14.1
|
||||||
|
|
@ -3365,12 +3346,6 @@ snapshots:
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
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: {}
|
bit-buffer@0.2.5: {}
|
||||||
|
|
||||||
brace-expansion@1.1.11:
|
brace-expansion@1.1.11:
|
||||||
|
|
@ -3434,6 +3409,8 @@ snapshots:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
color-string: 1.9.1
|
color-string: 1.9.1
|
||||||
|
|
||||||
|
compute-scroll-into-view@3.1.1: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
convert-source-map@1.9.0: {}
|
convert-source-map@1.9.0: {}
|
||||||
|
|
@ -3513,6 +3490,15 @@ snapshots:
|
||||||
'@babel/runtime': 7.27.0
|
'@babel/runtime': 7.27.0
|
||||||
csstype: 3.1.3
|
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:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
|
@ -4486,6 +4472,8 @@ snapshots:
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
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):
|
react-redux@9.2.0(@types/react@19.1.0)(react@19.1.0)(redux@5.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/use-sync-external-store': 0.0.6
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
|
@ -4521,6 +4509,11 @@ snapshots:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(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: {}
|
react@19.1.0: {}
|
||||||
|
|
||||||
redux@5.0.1: {}
|
redux@5.0.1: {}
|
||||||
|
|
@ -4590,8 +4583,6 @@ snapshots:
|
||||||
|
|
||||||
scheduler@0.26.0: {}
|
scheduler@0.26.0: {}
|
||||||
|
|
||||||
sdp@3.2.0: {}
|
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.7.1: {}
|
semver@7.7.1: {}
|
||||||
|
|
@ -4817,8 +4808,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
type-fest@4.39.1: {}
|
|
||||||
|
|
||||||
typed-array-buffer@1.0.3:
|
typed-array-buffer@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
|
|
@ -4895,10 +4884,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
webrtc-adapter@9.0.1:
|
|
||||||
dependencies:
|
|
||||||
sdp: 3.2.0
|
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.1.0
|
is-bigint: 1.1.0
|
||||||
|
|
@ -4951,8 +4936,3 @@ snapshots:
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.24.2: {}
|
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>
|
<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} />
|
<Icon icon="mdi:camera" fontSize={20} />
|
||||||
Use your camera
|
Use your camera
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
|
import Webcam from "react-webcam";
|
||||||
|
import jsQR from "jsqr";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import QrFinder from "../qr-finder";
|
import QrFinder from "./qr-finder";
|
||||||
|
import { useSelect } from "downshift";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -15,62 +17,164 @@ interface Props {
|
||||||
export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
const [permissionGranted, setPermissionGranted] = useState<boolean | null>(null);
|
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;
|
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
|
navigator.mediaDevices
|
||||||
.getUserMedia({ video: true })
|
.getUserMedia({ video: true })
|
||||||
.then(() => setPermissionGranted(true))
|
.then(() => setPermissionGranted(true))
|
||||||
.catch(() => setPermissionGranted(false));
|
.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]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleScan = (result: IDetectedBarcode[]) => {
|
useEffect(() => {
|
||||||
// todo: fix scan, use jsQR instead, data is wrong
|
if (!isOpen && !permissionGranted) return;
|
||||||
setIsOpen(false);
|
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||||
|
}, [permissionGranted]);
|
||||||
// Convert to bytes
|
|
||||||
// const encoder = new TextEncoder();
|
|
||||||
// const byteArray = encoder.encode(result[0].rawValue);
|
|
||||||
|
|
||||||
// setQrBytes(byteArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isOpen)
|
if (isOpen)
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-40 backdrop-brightness-75 backdrop-blur-xs">
|
<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="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>
|
<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">
|
<button onClick={() => setIsOpen(false)} className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
||||||
<Icon icon="material-symbols:close-rounded" />
|
<Icon icon="material-symbols:close-rounded" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="relative w-full aspect-square">
|
||||||
{permissionGranted === null ? (
|
{!permissionGranted ? (
|
||||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg border-2 border-amber-500">
|
<div className="absolute inset-0 flex flex-col items-center justify-center rounded-2xl border-2 border-amber-500 text-center p-8">
|
||||||
<div className="text-center p-4">
|
<p className="text-red-400 font-bold text-lg mb-2">Camera access denied</p>
|
||||||
<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>
|
||||||
<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">
|
||||||
</div>
|
Request Permission
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Scanner
|
<Webcam
|
||||||
formats={["qr_code"]}
|
ref={webcamRef}
|
||||||
onScan={handleScan}
|
audio={false}
|
||||||
components={{ finder: false }}
|
videoConstraints={{
|
||||||
sound={false}
|
deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined,
|
||||||
classNames={{ container: "rounded-lg border-2 border-amber-500" }}
|
}}
|
||||||
|
className="size-full object-cover rounded-2xl border-2 border-amber-500"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QrFinder />
|
<QrFinder />
|
||||||
|
<canvas ref={canvasRef} className="hidden" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-4 flex justify-center">
|
||||||
<button onClick={() => setIsOpen(false)} className="pill button">
|
<button type="button" onClick={() => setIsOpen(false)} className="pill button">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { FileWithPath, useDropzone } from "react-dropzone";
|
import { FileWithPath, useDropzone } from "react-dropzone";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import jsQR from "jsqr";
|
import jsQR from "jsqr";
|
||||||
|
|
@ -10,6 +10,8 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QrUpload({ setQrBytesRaw }: Props) {
|
export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
|
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
|
||||||
acceptedFiles.forEach((file) => {
|
acceptedFiles.forEach((file) => {
|
||||||
// Scan QR code
|
// Scan QR code
|
||||||
|
|
@ -17,7 +19,9 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
|
|
@ -26,9 +30,10 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||||
|
|
||||||
const imageData = ctx.getImageData(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;
|
image.src = event.target!.result as string;
|
||||||
};
|
};
|
||||||
|
|
@ -59,6 +64,8 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||||
or click to open
|
or click to open
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<canvas ref={canvasRef} className="hidden" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue