move-all-books-from-shelf #15
9 changed files with 238 additions and 88 deletions
42
frontend/package-lock.json
generated
42
frontend/package-lock.json
generated
|
@ -9,7 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ericblade/quagga2": "^1.8.4",
|
||||
"@tanstack/react-query": "^5.52.1",
|
||||
"@tanstack/react-query": "^5.59.15",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
|
@ -22,6 +22,7 @@
|
|||
"date-fns": "^3.6.0",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"history": "^5.3.0",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-dom": "^18.3.1",
|
||||
|
@ -3727,9 +3728,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.56.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz",
|
||||
"integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==",
|
||||
"version": "5.59.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz",
|
||||
"integrity": "sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
@ -3737,12 +3738,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.56.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz",
|
||||
"integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==",
|
||||
"version": "5.59.15",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.15.tgz",
|
||||
"integrity": "sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.56.2"
|
||||
"@tanstack/query-core": "5.59.13"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
@ -6655,6 +6656,12 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/cssfontparser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
|
||||
"integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cssnano": {
|
||||
"version": "5.1.15",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
|
||||
|
@ -10610,6 +10617,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jest-canvas-mock": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
|
||||
"integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssfontparser": "^1.2.1",
|
||||
"moo-color": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-changed-files": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
|
||||
|
@ -12147,6 +12164,15 @@
|
|||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moo-color": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
|
||||
"integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"proxy": "http://localhost:3001",
|
||||
"dependencies": {
|
||||
"@ericblade/quagga2": "^1.8.4",
|
||||
"@tanstack/react-query": "^5.52.1",
|
||||
"@tanstack/react-query": "^5.59.15",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
|
@ -18,6 +18,7 @@
|
|||
"date-fns": "^3.6.0",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"history": "^5.3.0",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
import { QuaggaJSResultObject } from "@ericblade/quagga2";
|
||||
import { getMedianOfCodeErrors } from "../getMedianOfCodeErrors";
|
||||
|
||||
export const createMockQuaggaResult = (
|
||||
decodedCodes: Array<{
|
||||
error?: number;
|
||||
code: number;
|
||||
start: number;
|
||||
end: number;
|
||||
}> = []
|
||||
): QuaggaJSResultObject => ({
|
||||
codeResult: {
|
||||
code: "123456",
|
||||
start: 0,
|
||||
end: 10,
|
||||
codeset: 1,
|
||||
startInfo: {
|
||||
error: 0,
|
||||
code: 1,
|
||||
start: 0,
|
||||
end: 5,
|
||||
},
|
||||
decodedCodes,
|
||||
endInfo: {
|
||||
error: 0,
|
||||
code: 1,
|
||||
start: 5,
|
||||
end: 10,
|
||||
},
|
||||
direction: 1,
|
||||
format: "QR",
|
||||
},
|
||||
barcodes: [],
|
||||
line: [{ x: 0, y: 0 }],
|
||||
angle: 0,
|
||||
pattern: [],
|
||||
box: [],
|
||||
boxes: [],
|
||||
});
|
||||
|
||||
describe("getMedianOfCodeErrors", () => {
|
||||
it("should return the median of code errors when there are valid errors", () => {
|
||||
const result = createMockQuaggaResult([
|
||||
{ error: 1, code: 1, start: 0, end: 5 },
|
||||
{ error: 3, code: 1, start: 5, end: 10 },
|
||||
{ error: 2, code: 1, start: 0, end: 10 },
|
||||
]);
|
||||
|
||||
const median = getMedianOfCodeErrors(result);
|
||||
expect(median).toBe(2); // The median of [1, 3, 2] is 2
|
||||
});
|
||||
|
||||
it("should return undefined when there are no code errors", () => {
|
||||
const result = createMockQuaggaResult([
|
||||
{ code: 1, start: 0, end: 5 }, // No error property
|
||||
{ code: 1, start: 5, end: 10 },
|
||||
]);
|
||||
|
||||
const median = getMedianOfCodeErrors(result);
|
||||
expect(median).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle an empty decodedCodes array", () => {
|
||||
const result = createMockQuaggaResult([]);
|
||||
|
||||
const median = getMedianOfCodeErrors(result);
|
||||
expect(median).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import React from "react";
|
||||
import { render, waitFor, cleanup } from "@testing-library/react";
|
||||
import { AuthContext } from "../../../App";
|
||||
import { useAuth } from "../useAuthentication";
|
||||
|
||||
const MockedComponent: React.FC = () => {
|
||||
const { authenticated } = useAuth();
|
||||
return <div>{authenticated}</div>;
|
||||
};
|
||||
|
||||
let authenticated = false;
|
||||
const setAuthenticated = (args: boolean) => (authenticated = args);
|
||||
|
||||
describe("useAuth", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("should set authenticated to true when fetch returns ok", async () => {
|
||||
global.fetch = jest.fn(() => Promise.resolve({ ok: true })) as jest.Mock;
|
||||
|
||||
render(
|
||||
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
|
||||
<MockedComponent />
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(authenticated).toBe(true);
|
||||
});
|
||||
|
||||
it("should set authenticated to false when fetch returns not ok", async () => {
|
||||
global.fetch = jest.fn(() => Promise.resolve({ ok: false })) as jest.Mock;
|
||||
|
||||
render(
|
||||
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
|
||||
<MockedComponent />
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(authenticated).toBe(false);
|
||||
});
|
||||
|
||||
it("should call fetch once", async () => {
|
||||
global.fetch = jest.fn(() => Promise.resolve({ ok: false })) as jest.Mock;
|
||||
|
||||
render(
|
||||
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
|
||||
<MockedComponent />
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
34
frontend/src/shared/utils/drawQuaggaProcessing.ts
Normal file
34
frontend/src/shared/utils/drawQuaggaProcessing.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Quagga, { QuaggaJSResultObject } from "@ericblade/quagga2";
|
||||
|
||||
export const drawQuaggaProcessing = (result: QuaggaJSResultObject) => {
|
||||
const drawingCtx = Quagga.canvas.ctx.overlay;
|
||||
const drawingCanvas = Quagga.canvas.dom.overlay;
|
||||
drawingCtx.font = "24px Arial";
|
||||
drawingCtx.fillStyle = "green";
|
||||
const width = drawingCanvas.getAttribute("width");
|
||||
const height = drawingCanvas.getAttribute("height");
|
||||
|
||||
if (result) {
|
||||
if (result.boxes && width && height) {
|
||||
drawingCtx.clearRect(0, 0, parseInt(width), parseInt(height));
|
||||
result.boxes
|
||||
.filter((box) => box !== result.box)
|
||||
.forEach((box) => {
|
||||
Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
|
||||
color: "purple",
|
||||
lineWidth: 2,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (result.box) {
|
||||
Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
|
||||
color: "blue",
|
||||
lineWidth: 2,
|
||||
});
|
||||
}
|
||||
if (result.codeResult && result.codeResult.code) {
|
||||
drawingCtx.font = "24px Arial";
|
||||
drawingCtx.fillText(result.codeResult.code, 10, 20);
|
||||
}
|
||||
}
|
||||
};
|
20
frontend/src/shared/utils/getMedianOfCodeErrors.ts
Normal file
20
frontend/src/shared/utils/getMedianOfCodeErrors.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { QuaggaJSResultObject } from "@ericblade/quagga2";
|
||||
|
||||
export const getMedianOfCodeErrors = (result: QuaggaJSResultObject) => {
|
||||
const errors = result.codeResult.decodedCodes.flatMap((x) => x.error);
|
||||
const medianOfErrors = getMedian(errors);
|
||||
return medianOfErrors;
|
||||
};
|
||||
|
||||
export const getMedian = (arr: (number | undefined)[]) => {
|
||||
const newArr = [...arr].filter((x) => typeof x !== "undefined");
|
||||
if (!newArr.length) {
|
||||
return;
|
||||
}
|
||||
newArr.sort((a, b) => a! - b!);
|
||||
const half = Math.floor(newArr.length / 2);
|
||||
if (newArr.length % 2 === 1) {
|
||||
return newArr[half];
|
||||
}
|
||||
return (newArr[half - 1]! + newArr[half]!) / 2;
|
||||
};
|
|
@ -1,11 +1,10 @@
|
|||
import { useCallback, useContext } from "react";
|
||||
import { useCallback, useContext, useEffect } from "react";
|
||||
import { AuthContext } from "../../App";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type UseAuthHook = { authenticated: boolean };
|
||||
|
||||
export const useAuth = (): UseAuthHook => {
|
||||
const auth = useContext(AuthContext);
|
||||
const { authenticated, setAuthenticated } = useContext(AuthContext);
|
||||
|
||||
const authenticationCheck = useCallback(
|
||||
async () =>
|
||||
|
@ -16,17 +15,14 @@ export const useAuth = (): UseAuthHook => {
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => {
|
||||
auth.setAuthenticated?.(res.ok);
|
||||
//react-query can't handle undefined without throwing a warning ...
|
||||
return null;
|
||||
setAuthenticated?.(res.ok);
|
||||
}),
|
||||
[auth]
|
||||
[setAuthenticated]
|
||||
);
|
||||
|
||||
useQuery({
|
||||
queryFn: authenticationCheck,
|
||||
queryKey: ["auth"],
|
||||
});
|
||||
useEffect(() => {
|
||||
authenticationCheck();
|
||||
}, [authenticationCheck]);
|
||||
|
||||
return auth;
|
||||
return { authenticated };
|
||||
};
|
||||
|
|
|
@ -3,6 +3,8 @@ import Quagga, {
|
|||
QuaggaJSResultObject,
|
||||
} from "@ericblade/quagga2";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { getMedianOfCodeErrors } from "./getMedianOfCodeErrors";
|
||||
import { drawQuaggaProcessing } from "./drawQuaggaProcessing";
|
||||
|
||||
const constraints = {
|
||||
width: 640,
|
||||
|
@ -29,28 +31,6 @@ export const useScanner = ({
|
|||
|
||||
const [scannerRef, setScannerRef] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const getMedian = useCallback((arr: (number | undefined)[]) => {
|
||||
const newArr = [...arr].filter((x) => typeof x !== "undefined");
|
||||
if (!newArr.length) {
|
||||
return;
|
||||
}
|
||||
newArr.sort((a, b) => a! - b!);
|
||||
const half = Math.floor(newArr.length / 2);
|
||||
if (newArr.length % 2 === 1) {
|
||||
return newArr[half];
|
||||
}
|
||||
return (newArr[half - 1]! + newArr[half]!) / 2;
|
||||
}, []);
|
||||
|
||||
const getMedianOfCodeErrors = useCallback(
|
||||
(result: QuaggaJSResultObject) => {
|
||||
const errors = result.codeResult.decodedCodes.flatMap((x) => x.error);
|
||||
const medianOfErrors = getMedian(errors);
|
||||
return medianOfErrors;
|
||||
},
|
||||
[getMedian]
|
||||
);
|
||||
|
||||
const errorCheck = useCallback(
|
||||
(result: QuaggaJSResultObject) => {
|
||||
if (!onDetected) {
|
||||
|
@ -66,42 +46,9 @@ export const useScanner = ({
|
|||
onDetected(result.codeResult.code);
|
||||
}
|
||||
},
|
||||
[getMedianOfCodeErrors, onDetected]
|
||||
[onDetected]
|
||||
);
|
||||
|
||||
const handleProcessed = (result: QuaggaJSResultObject) => {
|
||||
const drawingCtx = Quagga.canvas.ctx.overlay;
|
||||
const drawingCanvas = Quagga.canvas.dom.overlay;
|
||||
drawingCtx.font = "24px Arial";
|
||||
drawingCtx.fillStyle = "green";
|
||||
const width = drawingCanvas.getAttribute("width");
|
||||
const height = drawingCanvas.getAttribute("height");
|
||||
|
||||
if (result) {
|
||||
if (result.boxes && width && height) {
|
||||
drawingCtx.clearRect(0, 0, parseInt(width), parseInt(height));
|
||||
result.boxes
|
||||
.filter((box) => box !== result.box)
|
||||
.forEach((box) => {
|
||||
Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
|
||||
color: "purple",
|
||||
lineWidth: 2,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (result.box) {
|
||||
Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
|
||||
color: "blue",
|
||||
lineWidth: 2,
|
||||
});
|
||||
}
|
||||
if (result.codeResult && result.codeResult.code) {
|
||||
drawingCtx.font = "24px Arial";
|
||||
drawingCtx.fillText(result.codeResult.code, 10, 20);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getCameraId = useCallback(async () => {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment" },
|
||||
|
@ -150,7 +97,7 @@ export const useScanner = ({
|
|||
locate: true,
|
||||
},
|
||||
async (err) => {
|
||||
Quagga.onProcessed(handleProcessed);
|
||||
Quagga.onProcessed(drawQuaggaProcessing);
|
||||
|
||||
if (err) {
|
||||
return console.error("Error starting Quagga:", err);
|
||||
|
@ -170,7 +117,7 @@ export const useScanner = ({
|
|||
await Quagga.CameraAccess.release();
|
||||
await Quagga.stop();
|
||||
Quagga.offDetected(errorCheck);
|
||||
Quagga.offProcessed(handleProcessed);
|
||||
Quagga.offProcessed(drawQuaggaProcessing);
|
||||
}, [errorCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
Loading…
Reference in a new issue