Skip to content

Commit

Permalink
Merge pull request #879 from Shelf-nu/875-wip-bug-bugs-improvement-re…
Browse files Browse the repository at this point in the history
…lated-to-scanner

875 wip bug bugs improvement related to scanner
  • Loading branch information
DonKoko authored Mar 26, 2024
2 parents be8d1b7 + fb25a85 commit 8f249b1
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 112 deletions.
106 changes: 106 additions & 0 deletions app/components/zxing-scanner.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof loader>();
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 (
<div className="relative size-full min-h-[400px]">
{isProcessing ? (
<div className="mt-4 flex flex-col items-center justify-center">
<Spinner /> Switching cameras...
</div>
) : (
<>
<video
ref={ref}
width="100%"
autoPlay={true}
controls={false}
muted={true}
playsInline={true}
className={`pointer-events-none size-full object-cover object-center`}
/>
<fetcher.Form
method="post"
action="/api/user/prefs/scanner-camera"
className="relative"
onChange={(e) => {
const form = e.currentTarget;
fetcher.submit(form);
}}
>
{videoMediaDevices && videoMediaDevices?.length > 0 ? (
<select
className="absolute bottom-3 left-3 z-10 w-[calc(100%-24px)] rounded border-0 md:left-auto md:right-3 md:w-auto"
name="scannerCameraId"
defaultValue={scannerCameraId}
>
{videoMediaDevices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label ? device.label : `Camera ${index + 1}`}
</option>
))}
</select>
) : null}
</fetcher.Form>
</>
)}
</div>
);
};
71 changes: 1 addition & 70 deletions app/hooks/use-qr-scanner.ts
Original file line number Diff line number Diff line change
@@ -1,17 +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 = () => {
const navigate = useNavigate();
const [sendNotification] = useClientNotification();
const [selectedDevice, setSelectedDevice] = useState("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null);

const { devices } = useMediaDevices({
constraints: {
video: true,
Expand All @@ -22,7 +13,7 @@ export const useQrScanner = () => {
// 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) {
Expand All @@ -35,70 +26,10 @@ export const useQrScanner = () => {
}

setVideoMediaDevices(videoDevices);

// Set the selected device to the first video device
setSelectedDevice(videoDevices[0]?.deviceId || "");

// 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: selectedDevice,
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,
selectedDevice,
setSelectedDevice,
hasPermission,
};
};
59 changes: 19 additions & 40 deletions app/routes/_layout+/scanner.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import type {
LinksFunction,
LoaderFunctionArgs,
MetaFunction,
} from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link } from "@remix-run/react";
import { Link, useFetcher, useLoaderData } from "@remix-run/react";

Check warning on line 7 in app/routes/_layout+/scanner.tsx

View workflow job for this annotation

GitHub Actions / tests / ⬣ ESLint

'useFetcher' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 7 in app/routes/_layout+/scanner.tsx

View workflow job for this annotation

GitHub Actions / tests / ⬣ ESLint

'useLoaderData' is defined but never used. Allowed unused vars must match /^_/u
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";
import { userPrefs } from "~/utils/cookies.server";
import { makeShelfError } from "~/utils/error";
import { error } from "~/utils/http.server";

export const links: LinksFunction = () => [
{ rel: "stylesheet", href: scannerCss },
];

export function loader() {
export async function loader({ request }: LoaderFunctionArgs) {
try {
const header: HeaderData = {
title: "Locations",
};
return json({ header });

/** We get the userPrefs cookie so we can see if there is already a default camera */
const cookieHeader = request.headers.get("Cookie");
const cookie = (await userPrefs.parse(cookieHeader)) || {};

return json({ header, scannerCameraId: cookie.scannerCameraId });
} catch (cause) {
const reason = makeShelfError(cause);
throw json(error(reason), { status: reason.status });
Expand All @@ -36,50 +47,18 @@ export const meta: MetaFunction<typeof loader> = () => [
];

const QRScanner = () => {
const {
ref,
videoMediaDevices,
selectedDevice,
setSelectedDevice,
hasPermission,
} = useQrScanner();
const { videoMediaDevices } = useQrScanner();

return (
<>
<Header title="QR code scanner" />
<div className=" -mx-4 flex h-[calc(100vh-167px)] flex-col md:h-[calc(100vh-132px)]">
{!hasPermission ? (
{videoMediaDevices && videoMediaDevices.length > 0 ? (
<ZXingScanner videoMediaDevices={videoMediaDevices} />
) : (
<div className="mt-4 flex flex-col items-center justify-center">
<Spinner /> Waiting for permission to access camera.
</div>
) : (
<div className="relative size-full min-h-[400px]">
<video
ref={ref}
width="100%"
autoPlay={true}
controls={false}
muted={true}
playsInline={true}
className={`pointer-events-none size-full object-cover object-center`}
/>
{videoMediaDevices && videoMediaDevices?.length > 0 ? (
<select
className="absolute bottom-3 left-3 z-10 w-[calc(100%-24px)] rounded border-0 md:left-auto md:right-3 md:w-auto"
name="devices"
onChange={(e) => {
setSelectedDevice(e.currentTarget.value);
}}
defaultValue={selectedDevice}
>
{videoMediaDevices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label ? device.label : `Camera ${index + 1}`}
</option>
))}
</select>
) : null}
</div>
)}
</div>
</>
Expand Down
23 changes: 23 additions & 0 deletions app/routes/api+/user.prefs.scanner-camera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type ActionFunctionArgs, json } from "@remix-run/node";
import { data, error, makeShelfError } from "~/utils";
import { setCookie, userPrefs } from "~/utils/cookies.server";

export async function action({ context, request }: ActionFunctionArgs) {
const authSession = context.getSession();
const { userId } = authSession;

try {
const cookieHeader = request.headers.get("Cookie");
const cookie = (await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();

cookie.scannerCameraId = bodyParams.get("scannerCameraId");

return json(data({ success: true }), {
headers: [setCookie(await userPrefs.serialize(cookie))],
});
} catch (cause) {
const reason = makeShelfError(cause, { userId });
return json(error(reason), { status: reason.status });
}
}
5 changes: 3 additions & 2 deletions docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down

0 comments on commit 8f249b1

Please sign in to comment.