mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-28 11:13:16 +00:00
feat: replace react-webcam
This commit is contained in:
parent
df30270cc6
commit
98fffcc396
7 changed files with 62 additions and 74 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,6 +16,7 @@
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
certificates/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-webcam": "^7.2.0",
|
|
||||||
"redis": "^5.10.0",
|
"redis": "^5.10.0",
|
||||||
"satori": "^0.19.1",
|
"satori": "^0.19.1",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,6 @@ importers:
|
||||||
react-dropzone:
|
react-dropzone:
|
||||||
specifier: ^14.3.8
|
specifier: ^14.3.8
|
||||||
version: 14.3.8(react@19.2.4)
|
version: 14.3.8(react@19.2.4)
|
||||||
react-webcam:
|
|
||||||
specifier: ^7.2.0
|
|
||||||
version: 7.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
||||||
redis:
|
redis:
|
||||||
specifier: ^5.10.0
|
specifier: ^5.10.0
|
||||||
version: 5.10.0
|
version: 5.10.0
|
||||||
|
|
@ -2533,12 +2530,6 @@ packages:
|
||||||
redux:
|
redux:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
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.2.4:
|
react@19.2.4:
|
||||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -5299,11 +5290,6 @@ snapshots:
|
||||||
'@types/react': 19.2.10
|
'@types/react': 19.2.10
|
||||||
redux: 5.0.1
|
redux: 5.0.1
|
||||||
|
|
||||||
react-webcam@7.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
|
||||||
dependencies:
|
|
||||||
react: 19.2.4
|
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
|
||||||
|
|
||||||
react@19.2.4: {}
|
react@19.2.4: {}
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
|
||||||
|
|
@ -43,15 +43,15 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:disabled {
|
.button:disabled {
|
||||||
@apply text-zinc-600 !bg-zinc-100 !border-zinc-300 cursor-auto;
|
@apply text-zinc-600 bg-zinc-100! border-zinc-300! cursor-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@apply !bg-orange-200 outline-0 focus:ring-[3px] ring-orange-400/50 transition placeholder:text-black/40;
|
@apply bg-orange-200! outline-0 focus:ring-[3px] ring-orange-400/50 transition placeholder:text-black/40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:disabled {
|
.input:disabled {
|
||||||
@apply text-zinc-600 !bg-zinc-100 !border-zinc-300;
|
@apply text-zinc-600 bg-zinc-100! border-zinc-300!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,13 @@ export default async function SubmitPage() {
|
||||||
if (activePunishment) redirect("/off-the-island");
|
if (activePunishment) redirect("/off-the-island");
|
||||||
|
|
||||||
// Check if submissions are disabled
|
// Check if submissions are disabled
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
|
let value: boolean | null = true;
|
||||||
const { value } = await response.json();
|
try {
|
||||||
|
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
|
||||||
|
value = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
return <p>An error occurred!</p>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!value)
|
if (!value)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import { searchSchema } from "@/lib/schemas";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
import GenderSelect from "./gender-select";
|
|
||||||
import TagFilter from "./tag-filter";
|
|
||||||
import SortSelect from "./sort-select";
|
import SortSelect from "./sort-select";
|
||||||
import Carousel from "../carousel";
|
import Carousel from "../carousel";
|
||||||
import LikeButton from "../like-button";
|
import LikeButton from "../like-button";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import Webcam from "react-webcam";
|
|
||||||
import jsQR from "jsqr";
|
import jsQR from "jsqr";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
|
|
@ -22,7 +21,7 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
||||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
||||||
|
|
||||||
const webcamRef = useRef<Webcam>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const requestRef = useRef<number>(null);
|
const requestRef = useRef<number>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
|
@ -52,12 +51,9 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
// Continue scanning in a loop
|
// Continue scanning in a loop
|
||||||
requestRef.current = requestAnimationFrame(scanQRCode);
|
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||||
|
|
||||||
const webcam = webcamRef.current;
|
const video = videoRef.current;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!webcam || !canvas) return;
|
if (!video || video.videoWidth === 0 || video.videoHeight === 0 || !canvas) return;
|
||||||
|
|
||||||
const video = webcam.video;
|
|
||||||
if (!video || video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
@ -68,7 +64,7 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
|
|
||||||
const imageData = ctx.getImageData(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);
|
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||||
if (!code) return;
|
if (!code || !code.binaryData) return;
|
||||||
|
|
||||||
// Cancel animation frame to stop scanning
|
// Cancel animation frame to stop scanning
|
||||||
if (requestRef.current) {
|
if (requestRef.current) {
|
||||||
|
|
@ -76,15 +72,20 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
requestRef.current = null;
|
requestRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setQrBytesRaw(code.binaryData!);
|
setQrBytesRaw(code.binaryData);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, [isOpen, setIsOpen, setQrBytesRaw]);
|
}, [isOpen, setIsOpen, setQrBytesRaw]);
|
||||||
|
|
||||||
const requestPermission = async () => {
|
const requestPermission = () => {
|
||||||
|
if (!navigator.mediaDevices) return;
|
||||||
|
|
||||||
navigator.mediaDevices
|
navigator.mediaDevices
|
||||||
.getUserMedia({ video: true })
|
.getUserMedia({ video: true, audio: false })
|
||||||
.then(() => setPermissionGranted(true))
|
.then(() => setPermissionGranted(true))
|
||||||
.catch(() => setPermissionGranted(false));
|
.catch((err) => {
|
||||||
|
setPermissionGranted(false);
|
||||||
|
console.error("An error occurred trying to access the camera", err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
|
@ -98,34 +99,50 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
// slight delay to trigger animation
|
// slight delay to trigger animation
|
||||||
setTimeout(() => setIsVisible(true), 10);
|
setTimeout(() => setIsVisible(true), 10);
|
||||||
|
requestPermission();
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
requestPermission();
|
|
||||||
|
|
||||||
if (!navigator.mediaDevices.enumerateDevices) return;
|
|
||||||
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, selectedDeviceId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || !permissionGranted) return;
|
if (!isOpen || !permissionGranted) return;
|
||||||
|
|
||||||
|
navigator.mediaDevices
|
||||||
|
.enumerateDevices()
|
||||||
|
.then((devices) => {
|
||||||
|
const videoDevices = devices.filter((d) => d.kind === "videoinput");
|
||||||
|
setDevices(videoDevices);
|
||||||
|
|
||||||
|
const targetDeviceId = selectedDeviceId || videoDevices[0]?.deviceId;
|
||||||
|
if (!targetDeviceId) return;
|
||||||
|
setSelectedDeviceId(targetDeviceId);
|
||||||
|
|
||||||
|
// start camera stream
|
||||||
|
return navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { deviceId: targetDeviceId },
|
||||||
|
audio: false,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((stream) => {
|
||||||
|
if (!stream || !videoRef.current) return;
|
||||||
|
videoRef.current.srcObject = stream;
|
||||||
|
videoRef.current.play();
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Camera error", err));
|
||||||
|
|
||||||
requestRef.current = requestAnimationFrame(scanQRCode);
|
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
return () => {
|
return () => {
|
||||||
if (requestRef.current) {
|
if (requestRef.current) {
|
||||||
cancelAnimationFrame(requestRef.current);
|
cancelAnimationFrame(requestRef.current);
|
||||||
}
|
}
|
||||||
|
if (videoRef.current?.srcObject) {
|
||||||
|
const stream = videoRef.current.srcObject as MediaStream;
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
videoRef.current.srcObject = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [isOpen, permissionGranted, scanQRCode]);
|
}, [isOpen, permissionGranted, selectedDeviceId, scanQRCode]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
|
@ -133,9 +150,7 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40">
|
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40">
|
||||||
<div
|
<div
|
||||||
onClick={close}
|
onClick={close}
|
||||||
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${
|
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${isVisible ? "opacity-100" : "opacity-0"}`}
|
||||||
isVisible ? "opacity-100" : "opacity-0"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -189,35 +204,19 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative w-full aspect-square">
|
<div className="relative w-full aspect-square">
|
||||||
{!permissionGranted ? (
|
{!permissionGranted && (
|
||||||
<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="absolute inset-0 flex flex-col items-center justify-center rounded-2xl bg-amber-50 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-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!">
|
<button type="button" onClick={requestPermission} className="pill button text-xs mt-2 py-0.5! px-2!">
|
||||||
Request Permission
|
Request Permission
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Webcam
|
|
||||||
key={selectedDeviceId}
|
|
||||||
ref={webcamRef}
|
|
||||||
audio={false}
|
|
||||||
videoConstraints={{
|
|
||||||
deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined,
|
|
||||||
...(selectedDeviceId ? {} : { facingMode: { ideal: "environment" } }),
|
|
||||||
}}
|
|
||||||
onUserMedia={async () => {
|
|
||||||
const newDevices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
const videoDevices = newDevices.filter((d) => d.kind === "videoinput");
|
|
||||||
setDevices(videoDevices);
|
|
||||||
}}
|
|
||||||
className="size-full object-cover rounded-2xl border-2 border-amber-500"
|
|
||||||
/>
|
|
||||||
<QrFinder />
|
|
||||||
<canvas ref={canvasRef} className="hidden" />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<video ref={videoRef} className="size-full object-cover rounded-2xl border-2 border-amber-500" />
|
||||||
|
<QrFinder />
|
||||||
|
<canvas ref={canvasRef} className="hidden" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-4 flex justify-center">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue