diff --git a/app/components/zxing-scanner.tsx b/app/components/zxing-scanner.tsx new file mode 100644 index 000000000..522765046 --- /dev/null +++ b/app/components/zxing-scanner.tsx @@ -0,0 +1,106 @@ +import { useFetcher, useLoaderData, useNavigate } from "@remix-run/react"; +import { useZxing } from "react-zxing"; +import { useClientNotification } from "~/hooks/use-client-notification"; +import type { loader } from "~/routes/_layout+/scanner"; +import { isFormProcessing } from "~/utils"; +import { ShelfError } from "~/utils/error"; +import { Spinner } from "./shared/spinner"; + +export const ZXingScanner = ({ + videoMediaDevices, +}: { + videoMediaDevices: MediaDeviceInfo[] | undefined; +}) => { + const [sendNotification] = useClientNotification(); + const navigate = useNavigate(); + const fetcher = useFetcher(); + const { scannerCameraId } = useLoaderData(); + const isProcessing = isFormProcessing(fetcher.state); + + // Function to decode the QR code + const decodeQRCodes = (result: string) => { + if (result != null) { + const regex = /^(https?:\/\/)([^/:]+)(:\d+)?\/qr\/([a-zA-Z0-9]+)$/; + /** We make sure the value of the QR code matches the structure of Shelf qr codes */ + const match = result.match(regex); + if (!match) { + /** If the QR code does not match the structure of Shelf qr codes, we show an error message */ + sendNotification({ + title: "QR Code Not Valid", + message: "Please Scan valid asset QR", + icon: { name: "trash", variant: "error" }, + }); + return; + } + + sendNotification({ + title: "Shelf's QR Code detected", + message: "Redirecting to mapped asset", + icon: { name: "success", variant: "success" }, + }); + const qrId = match[4]; // Get the last segment of the URL as the QR id + navigate(`/qr/${qrId}`); + } + }; + + const { ref } = useZxing({ + deviceId: scannerCameraId, + constraints: { video: true, audio: false }, + onDecodeResult(result) { + decodeQRCodes(result.getText()); + }, + onError(cause) { + throw new ShelfError({ + message: "Unable to access media devices permission", + status: 403, + label: "Scanner", + cause, + }); + }, + }); + + return ( +
+ {isProcessing ? ( +
+ Switching cameras... +
+ ) : ( + <> +
+ ); +}; diff --git a/app/hooks/use-qr-scanner.ts b/app/hooks/use-qr-scanner.ts index ae2392d93..5373a011d 100644 --- a/app/hooks/use-qr-scanner.ts +++ b/app/hooks/use-qr-scanner.ts @@ -1,20 +1,8 @@ import { useEffect, useState } from "react"; -import { useNavigate } from "@remix-run/react"; import { useMediaDevices } from "react-media-devices"; -import { useZxing } from "react-zxing"; -import { ShelfError } from "~/utils"; -import { useClientNotification } from "./use-client-notification"; // Custom hook to handle video devices -export const useQrScanner = (scannerCameraId: string) => { - const navigate = useNavigate(); - /** Get the default id from the loader - * This is the default camera that the user has selected on previous use of the scanner - * It comes from a cookie - */ - const [sendNotification] = useClientNotification(); - // const [hasPermission, setHasPermission] = useState(null); - +export const useQrScanner = () => { const { devices } = useMediaDevices({ constraints: { video: true, @@ -25,7 +13,7 @@ export const useQrScanner = (scannerCameraId: string) => { // Initialize videoMediaDevices as undefined. This will be used to store the video devices once they have loaded. const [videoMediaDevices, setVideoMediaDevices] = useState< MediaDeviceInfo[] | undefined - >(); + >(undefined); useEffect(() => { if (devices) { @@ -38,65 +26,10 @@ export const useQrScanner = (scannerCameraId: string) => { } setVideoMediaDevices(videoDevices); - - // Set hasPermission to true as devices are available - // setHasPermission(true); - } else { - // Set hasPermission to false as devices are not available - // setHasPermission(false); } }, [devices]); - // Use the useZxing hook to access the camera and scan for QR codes - const { ref } = useZxing({ - deviceId: scannerCameraId, - constraints: { video: true, audio: false }, - onDecodeResult(result) { - decodeQRCodes(result.getText()); - }, - - onError(cause) { - /** This is not idea an kinda useless actually - * We are simply showing the message to the user based on hasPermission so if they deny permission we show a message - */ - throw new ShelfError({ - message: "Unable to access media devices permission", - status: 403, - label: "Scanner", - cause, - }); - }, - }); - - // Function to decode the QR code - const decodeQRCodes = (result: string) => { - if (result != null) { - const regex = /^(https?:\/\/)([^/:]+)(:\d+)?\/qr\/([a-zA-Z0-9]+)$/; - /** We make sure the value of the QR code matches the structure of Shelf qr codes */ - const match = result.match(regex); - if (!match) { - /** If the QR code does not match the structure of Shelf qr codes, we show an error message */ - sendNotification({ - title: "QR Code Not Valid", - message: "Please Scan valid asset QR", - icon: { name: "trash", variant: "error" }, - }); - return; - } - - sendNotification({ - title: "Shelf's QR Code detected", - message: "Redirecting to mapped asset", - icon: { name: "success", variant: "success" }, - }); - const qrId = match[4]; // Get the last segment of the URL as the QR id - navigate(`/qr/${qrId}`); - } - }; - return { - ref, videoMediaDevices, - // hasPermission, }; }; diff --git a/app/routes/_layout+/scanner.tsx b/app/routes/_layout+/scanner.tsx index eccfe7bd8..4cf8168af 100644 --- a/app/routes/_layout+/scanner.tsx +++ b/app/routes/_layout+/scanner.tsx @@ -8,6 +8,8 @@ import { Link, useFetcher, useLoaderData } from "@remix-run/react"; import { ErrorContent } from "~/components/errors"; import Header from "~/components/layout/header"; import type { HeaderData } from "~/components/layout/header/types"; +import { Spinner } from "~/components/shared/spinner"; +import { ZXingScanner } from "~/components/zxing-scanner"; import { useQrScanner } from "~/hooks/use-qr-scanner"; import scannerCss from "~/styles/scanner.css"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; @@ -45,55 +47,19 @@ export const meta: MetaFunction = () => [ ]; const QRScanner = () => { - const fetcher = useFetcher(); - const { scannerCameraId } = useLoaderData(); - - const { ref, videoMediaDevices } = useQrScanner(scannerCameraId); + const { videoMediaDevices } = useQrScanner(); return ( <>
- {/* {!videoMediaDevices || videoMediaDevices.length === 0 ? ( + {videoMediaDevices && videoMediaDevices.length > 0 ? ( + + ) : (
Waiting for permission to access camera.
- ) : ( */} -
-
- {/* )} */} + )}
); diff --git a/docs/docker.md b/docs/docker.md index b368fd7de..7bcbb361d 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -36,8 +36,7 @@ This will make sure you have a DATABASE that you are ready to connect to. --restart unless-stopped \ ghcr.io/shelf-nu/shelf.nu:latest ``` -> [!NOTE] -> `DATABASE_URL` and `DIRECT_URL` are mandatory when using Supabase Cloud. Learn more in [Get Started > Development](./get-started.md#development) section. + > [!NOTE] > `DATABASE_URL` and `DIRECT_URL` are mandatory when using Supabase Cloud. Learn more in [Get Started > Development](./get-started.md#development) section. 3. Run the following command to seed the database (create initial user), **only once after the first deployment**: ```sh docker exec -it shelf npm run setup:seed @@ -47,6 +46,7 @@ This will make sure you have a DATABASE that you are ready to connect to. > [!CAUTION] > During development involving Dockerfile changes, make sure to **address the correct Dockerfile** in your builds: +> > - Fly.io will be built via `Dockerfile` > - ghcr.io will be built via `Dockerfile.image` @@ -75,6 +75,7 @@ docker run -d \ You can also run shelf on ARM64 processors. 1. Linux / Pine A64 + ```sh root@DietPi:~# docker run -it --rm --entrypoint /usr/bin/uname ghcr.io/shelf-nu/shelf.nu:latest -a diff --git a/docs/get-started.md b/docs/get-started.md index 350b7443b..714e8f3bc 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -109,6 +109,7 @@ The database seed script creates a new user with some data you can use to get st > [!CAUTION] > During development involving Dockerfile changes, make sure to **address the correct file** in your builds: +> > - Fly.io will be built via `Dockerfile` > - ghcr.io will be built via `Dockerfile.image`