diff --git a/.gitignore b/.gitignore index c058940..e9e17a1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ # next.js /.next/ /out/ +certificates/ # production /build diff --git a/package.json b/package.json index 3840a29..5299718 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-dropzone": "^14.3.8", - "react-webcam": "^7.2.0", "redis": "^5.10.0", "satori": "^0.19.1", "seedrandom": "^3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c6194a..c8dd795 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,6 @@ importers: react-dropzone: specifier: ^14.3.8 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: specifier: ^5.10.0 version: 5.10.0 @@ -2533,12 +2530,6 @@ packages: redux: 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: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -5299,11 +5290,6 @@ snapshots: '@types/react': 19.2.10 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: {} readdirp@4.1.2: {} diff --git a/src/app/globals.css b/src/app/globals.css index ac3d5b8..6131603 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -43,15 +43,15 @@ body { } .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 { - @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 { - @apply text-zinc-600 !bg-zinc-100 !border-zinc-300; + @apply text-zinc-600 bg-zinc-100! border-zinc-300!; } .checkbox { diff --git a/src/app/submit/page.tsx b/src/app/submit/page.tsx index 0cd7a47..f2c63dd 100644 --- a/src/app/submit/page.tsx +++ b/src/app/submit/page.tsx @@ -32,8 +32,13 @@ export default async function SubmitPage() { if (activePunishment) redirect("/off-the-island"); // Check if submissions are disabled - const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`); - const { value } = await response.json(); + let value: boolean | null = true; + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`); + value = await response.json(); + } catch (error) { + return

An error occurred!

; + } if (!value) return ( diff --git a/src/components/mii-list/index.tsx b/src/components/mii-list/index.tsx index dd8d185..f45bb7d 100644 --- a/src/components/mii-list/index.tsx +++ b/src/components/mii-list/index.tsx @@ -10,8 +10,6 @@ import { searchSchema } from "@/lib/schemas"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; -import GenderSelect from "./gender-select"; -import TagFilter from "./tag-filter"; import SortSelect from "./sort-select"; import Carousel from "../carousel"; import LikeButton from "../like-button"; diff --git a/src/components/submit-form/qr-scanner.tsx b/src/components/submit-form/qr-scanner.tsx index 103dc95..91f0d26 100644 --- a/src/components/submit-form/qr-scanner.tsx +++ b/src/components/submit-form/qr-scanner.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import Webcam from "react-webcam"; import jsQR from "jsqr"; import { Icon } from "@iconify/react"; @@ -22,7 +21,7 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) { const [devices, setDevices] = useState([]); const [selectedDeviceId, setSelectedDeviceId] = useState(null); - const webcamRef = useRef(null); + const videoRef = useRef(null); const requestRef = useRef(null); const canvasRef = useRef(null); @@ -52,12 +51,9 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) { // Continue scanning in a loop requestRef.current = requestAnimationFrame(scanQRCode); - const webcam = webcamRef.current; + const video = videoRef.current; const canvas = canvasRef.current; - if (!webcam || !canvas) return; - - const video = webcam.video; - if (!video || video.videoWidth === 0 || video.videoHeight === 0) return; + if (!video || video.videoWidth === 0 || video.videoHeight === 0 || !canvas) return; const ctx = canvas.getContext("2d"); 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 code = jsQR(imageData.data, imageData.width, imageData.height); - if (!code) return; + if (!code || !code.binaryData) return; // Cancel animation frame to stop scanning if (requestRef.current) { @@ -76,15 +72,20 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) { requestRef.current = null; } - setQrBytesRaw(code.binaryData!); + setQrBytesRaw(code.binaryData); setIsOpen(false); }, [isOpen, setIsOpen, setQrBytesRaw]); - const requestPermission = async () => { + const requestPermission = () => { + if (!navigator.mediaDevices) return; + navigator.mediaDevices - .getUserMedia({ video: true }) + .getUserMedia({ video: true, audio: false }) .then(() => setPermissionGranted(true)) - .catch(() => setPermissionGranted(false)); + .catch((err) => { + setPermissionGranted(false); + console.error("An error occurred trying to access the camera", err); + }); }; const close = () => { @@ -98,34 +99,50 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) { if (isOpen) { // slight delay to trigger animation setTimeout(() => setIsVisible(true), 10); + requestPermission(); } }, [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(() => { 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); + // cleanup return () => { if (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; @@ -133,9 +150,7 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
- {!permissionGranted ? ( -
+ {!permissionGranted && ( +

Camera access denied

Please allow camera access in your browser settings to scan QR codes

- ) : ( - <> - { - 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" - /> - - - )} + +