feat: submit page qr code scanner

This commit is contained in:
trafficlunar 2025-04-02 19:09:42 +01:00
parent b35c0a53ea
commit 3df8a87c1c
7 changed files with 769 additions and 91 deletions

View file

@ -0,0 +1,20 @@
export default function QrFinder() {
return (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none size-72 z-10">
{/* Top-left corner */}
<div className="absolute top-0 left-0 size-6 border-t-3 border-l-3 border-amber-500 rounded-tl-lg" />
{/* Top-right corner */}
<div className="absolute top-0 right-0 size-6 border-t-3 border-r-3 border-amber-500 rounded-tr-lg" />
{/* Bottom-left corner */}
<div className="absolute bottom-0 left-0 size-6 border-b-3 border-l-3 border-amber-500 rounded-bl-lg" />
{/* Bottom-right corner */}
<div className="absolute bottom-0 right-0 size-6 border-b-3 border-r-3 border-amber-500 rounded-br-lg" />
{/* Center point */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-5 bg-amber-500/70 rounded-full" />
</div>
);
}

View file

@ -1,21 +1,12 @@
"use client";
import { useState } from "react";
import { useDropzone } from "react-dropzone";
import CreatableSelect from "react-select/creatable";
import { Icon } from "@iconify/react";
const options = [
{ value: "anime", label: "anime" },
{ value: "art", label: "art" },
{ value: "cartoon", label: "cartoon" },
{ value: "celebrity", label: "celebrity" },
{ value: "games", label: "games" },
{ value: "history", label: "history" },
{ value: "meme", label: "meme" },
{ value: "movie", label: "movie" },
{ value: "oc", label: "oc" },
{ value: "tv", label: "tv" },
];
import TagSelector from "./submit/tag-selector";
import QrUpload from "./submit/qr-upload";
import QrScanner from "./submit/qr-scanner";
export default function SubmitForm() {
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
@ -24,7 +15,8 @@ export default function SubmitForm() {
},
});
// todo: tag validating
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
const [qrBytes, setQrBytes] = useState<Uint8Array>(new Uint8Array());
return (
<form onSubmit={(e) => e.preventDefault()} className="grid grid-cols-2">
@ -68,94 +60,22 @@ export default function SubmitForm() {
<label htmlFor="tags" className="font-semibold">
Tags
</label>
<CreatableSelect
isMulti
placeholder="Select or create tags..."
options={options}
className="pill input col-span-2 w-full !py-0.5"
styles={{
control: (provided) => ({
...provided,
border: "none",
background: "transparent",
width: "100%",
boxShadow: "none",
}),
valueContainer: (provided) => ({
...provided,
padding: "0",
}),
multiValue: (provided) => ({
...provided,
borderRadius: "16px",
padding: "2px 8px",
backgroundColor: "var(--color-orange-300)",
}),
multiValueRemove: (provided) => ({
...provided,
cursor: "pointer",
"&:hover": {
backgroundColor: "transparent",
color: "var(--color-black)",
},
}),
indicatorsContainer: (provided) => ({
...provided,
"*": {
padding: "1px",
color: "black",
cursor: "pointer",
},
}),
indicatorSeparator: () => ({
display: "none",
}),
placeholder: (provided) => ({
...provided,
color: "rgba(0, 0, 0, 0.4)",
}),
menu: (provided) => ({
...provided,
backgroundColor: "var(--color-orange-200)",
border: "2px solid var(--color-orange-400)",
borderRadius: "8px",
}),
option: (provided, { isFocused }) => ({
...provided,
backgroundColor: isFocused ? "rgba(0, 0, 0, 0.15)" : "var(--color-orange-200)",
cursor: "pointer",
padding: "2px 8px",
}),
}}
/>
<TagSelector />
</div>
<fieldset className="border-t-2 border-b-2 border-black p-3 flex flex-col items-center gap-2">
<legend className="px-2">QR Code</legend>
<div className="p-2 border-2 bg-orange-100 border-amber-500 rounded-2xl shadow-lg w-full">
<div
{...getRootProps({
className:
"bg-orange-100 flex flex-col justify-center items-center gap-2 p-4 rounded-xl border border-2 border-dashed border-amber-500 select-none h-full",
})}
>
<input {...getInputProps({ multiple: false })} />
<Icon icon="material-symbols:upload" fontSize={48} />
<p className="text-center text-sm">
Drag and drop your QR code image here
<br />
or click to open
</p>
</div>
</div>
<QrUpload />
<span>or</span>
<button className="pill button gap-2">
<button onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2">
<Icon icon="mdi:camera" fontSize={20} />
Use your camera
</button>
<QrScanner isOpen={isQrScannerOpen} setIsOpen={setIsQrScannerOpen} setQrBytes={setQrBytes} />
</fieldset>
<button type="submit" className="pill button w-min ml-auto">

View file

@ -0,0 +1,79 @@
"use client";
import { useEffect, useState } from "react";
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
import { Icon } from "@iconify/react";
import QrFinder from "../qr-finder";
interface Props {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setQrBytes: React.Dispatch<React.SetStateAction<Uint8Array>>;
}
export default function QrScanner({ isOpen, setIsOpen, setQrBytes }: Props) {
const [permissionGranted, setPermissionGranted] = useState<boolean | null>(null);
useEffect(() => {
if (!isOpen) return;
navigator.mediaDevices
.getUserMedia({ video: true })
.then(() => setPermissionGranted(true))
.catch(() => setPermissionGranted(false));
}, [isOpen]);
const handleScan = (result: IDetectedBarcode[]) => {
setIsOpen(false);
const encoder = new TextEncoder();
const byteArray = encoder.encode(result[0].rawValue);
setQrBytes(byteArray);
};
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">
<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>
<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>
</div>
) : (
<>
<Scanner
formats={["qr_code"]}
onScan={handleScan}
components={{ finder: false }}
sound={false}
classNames={{ container: "rounded-lg border-2 border-amber-500" }}
/>
<QrFinder />
</>
)}
</div>
<div className="mt-4 flex justify-center">
<button onClick={() => setIsOpen(false)} className="pill button">
Cancel
</button>
</div>
</div>
</div>
);
else return null;
}

View file

@ -0,0 +1,31 @@
"use client";
import { Icon } from "@iconify/react";
import { useDropzone } from "react-dropzone";
export default function QrUpload() {
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
accept: {
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"],
},
});
return (
<div className="p-2 border-2 bg-orange-100 border-amber-500 rounded-2xl shadow-lg w-full">
<div
{...getRootProps({
className:
"bg-orange-100 flex flex-col justify-center items-center gap-2 p-4 rounded-xl border border-2 border-dashed border-amber-500 select-none h-full",
})}
>
<input {...getInputProps({ multiple: false })} />
<Icon icon="material-symbols:upload" fontSize={48} />
<p className="text-center text-sm">
Drag and drop your QR code image here
<br />
or click to open
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,83 @@
"use client";
import CreatableSelect from "react-select/creatable";
const options = [
{ value: "anime", label: "anime" },
{ value: "art", label: "art" },
{ value: "cartoon", label: "cartoon" },
{ value: "celebrity", label: "celebrity" },
{ value: "games", label: "games" },
{ value: "history", label: "history" },
{ value: "meme", label: "meme" },
{ value: "movie", label: "movie" },
{ value: "oc", label: "oc" },
{ value: "tv", label: "tv" },
];
export default function TagSelector() {
// todo: tag validating
return (
<CreatableSelect
isMulti
placeholder="Select or create tags..."
options={options}
className="pill input col-span-2 w-full !py-0.5"
styles={{
control: (provided) => ({
...provided,
border: "none",
background: "transparent",
width: "100%",
boxShadow: "none",
}),
valueContainer: (provided) => ({
...provided,
padding: "0",
}),
multiValue: (provided) => ({
...provided,
borderRadius: "16px",
padding: "2px 8px",
backgroundColor: "var(--color-orange-300)",
}),
multiValueRemove: (provided) => ({
...provided,
cursor: "pointer",
"&:hover": {
backgroundColor: "transparent",
color: "var(--color-black)",
},
}),
indicatorsContainer: (provided) => ({
...provided,
"*": {
padding: "1px",
color: "black",
cursor: "pointer",
},
}),
indicatorSeparator: () => ({
display: "none",
}),
placeholder: (provided) => ({
...provided,
color: "rgba(0, 0, 0, 0.4)",
}),
menu: (provided) => ({
...provided,
backgroundColor: "var(--color-orange-200)",
border: "2px solid var(--color-orange-400)",
borderRadius: "8px",
}),
option: (provided, { isFocused }) => ({
...provided,
backgroundColor: isFocused ? "rgba(0, 0, 0, 0.15)" : "var(--color-orange-200)",
cursor: "pointer",
padding: "2px 8px",
}),
}}
/>
);
}