Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Commit

Permalink
feat: public blid search
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrianAndersen committed Nov 6, 2024
1 parent 42b58d6 commit 26370ea
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 119 deletions.
26 changes: 26 additions & 0 deletions src/app/sjekk/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Card, Container, Stack, Typography } from "@mui/material";
import { Box } from "@mui/system";
import { Metadata } from "next";
import React from "react";

import PublicBlidSearch from "@/components/search/PublicBlidSearch";

export const metadata: Metadata = {
title: "Boksøk",
description: "Sjekk hvem bøker utdelt fra Boklisten tilhører",
};

export default function PublicBlidSearchPage() {
return (
<Card sx={{ paddingBottom: 4 }}>
<Container component="main" maxWidth="xs">
<Stack alignItems={"center"} mt={4}>
<Typography variant="h1">Boksøk</Typography>
<Box width="100%">
<PublicBlidSearch />
</Box>
</Stack>
</Container>
</Card>
);
}
22 changes: 15 additions & 7 deletions src/components/RapidHandoutDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import useSWR from "swr";
import BlFetcher from "@/api/blFetcher";
import { ItemStatus } from "@/components/matches/matches-helper";
import MatchItemTable from "@/components/matches/MatchItemTable";
import ScannerModal from "@/components/matches/Scanner/ScannerModal";
import MatchScannerContent from "@/components/matches/MatchScannerContent";
import ScannerModal from "@/components/scanner/ScannerModal";
import BL_CONFIG from "@/utils/bl-config";

function calculateUnfulfilledOrderItems(orders: Order[]): OrderItem[] {
Expand Down Expand Up @@ -101,12 +102,19 @@ export default function RapidHandoutDetails({
handleClose={() => {
setScanModalOpen(false);
}}
itemStatuses={itemStatuses}
expectedItems={itemStatuses.map((itemStatus) => itemStatus.id)}
fulfilledItems={itemStatuses
.filter((itemStatus) => itemStatus.fulfilled)
.map((itemStatus) => itemStatus.id)}
/>
>
<MatchScannerContent
scannerOpen={scanModalOpen}
handleClose={() => {
setScanModalOpen(false);
}}
itemStatuses={itemStatuses}
expectedItems={itemStatuses.map((itemStatus) => itemStatus.id)}
fulfilledItems={itemStatuses
.filter((itemStatus) => itemStatus.fulfilled)
.map((itemStatus) => itemStatus.id)}
/>
</ScannerModal>
</>
);
}
50 changes: 50 additions & 0 deletions src/components/matches/MatchScannerContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Box } from "@mui/material";
import Typography from "@mui/material/Typography";
import React, { useEffect } from "react";

import { ItemStatus } from "@/components/matches/matches-helper";
import ProgressBar from "@/components/matches/matchesList/ProgressBar";
import MatchItemTable from "@/components/matches/MatchItemTable";

export default function MatchScannerContent({
expectedItems,
fulfilledItems,
itemStatuses,
scannerOpen,
handleClose,
}: {
itemStatuses: ItemStatus[];
expectedItems: string[];
fulfilledItems: string[];
scannerOpen: boolean;
handleClose: () => void;
}) {
useEffect(() => {
if (scannerOpen && expectedItems.length === fulfilledItems.length) {
handleClose();
}
}, [expectedItems.length, fulfilledItems.length, handleClose, scannerOpen]);
return (
<>
<Box width={0.9}>
<ProgressBar
percentComplete={(fulfilledItems.length * 100) / expectedItems.length}
subtitle={
<Typography textAlign={"center"}>
{fulfilledItems.length} av {expectedItems.length} bøker mottatt
</Typography>
}
/>
</Box>
<Box
sx={{
overflowY: "auto",
maxHeight: "30rem",
mt: 2,
}}
>
<MatchItemTable itemStatuses={itemStatuses} isSender={false} />
</Box>
</>
);
}
21 changes: 15 additions & 6 deletions src/components/matches/UserMatchDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import {
import { UserMatchTitle } from "@/components/matches/matchesList/helper";
import ProgressBar from "@/components/matches/matchesList/ProgressBar";
import MatchItemTable from "@/components/matches/MatchItemTable";
import MatchScannerContent from "@/components/matches/MatchScannerContent";
import MeetingInfo from "@/components/matches/MeetingInfo";
import OtherPersonContact from "@/components/matches/OtherPersonContact";
import ScannerModal from "@/components/matches/Scanner/ScannerModal";
import ScannerTutorial from "@/components/matches/Scanner/ScannerTutorial";
import ScannerModal from "@/components/scanner/ScannerModal";
import ScannerTutorial from "@/components/scanner/ScannerTutorial";
import BL_CONFIG from "@/utils/bl-config";
import { UserMatchWithDetails } from "@/utils/types";

Expand Down Expand Up @@ -160,10 +161,18 @@ const UserMatchDetail = ({
setScanModalOpen(false);
setRedirectCountdownStarted(isFulfilled);
}}
itemStatuses={itemStatuses}
expectedItems={match.expectedItems}
fulfilledItems={fulfilledItems}
/>
>
<MatchScannerContent
handleClose={() => {
setScanModalOpen(false);
setRedirectCountdownStarted(isFulfilled);
}}
scannerOpen={scanModalOpen}
itemStatuses={itemStatuses}
expectedItems={match.expectedItems}
fulfilledItems={fulfilledItems}
/>
</ScannerModal>
</>
);
};
Expand Down
53 changes: 53 additions & 0 deletions src/components/scanner/BlidScanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";

import { ScannedTextType, TextType } from "@/utils/types";

export function determineScannedTextType(scannedText: string): ScannedTextType {
if (/^[\dA-Za-z]{12}$|^\d{8}$/.test(scannedText)) {
return TextType.BLID;
} else if (/^\d{13}$/.test(scannedText)) {
return TextType.ISBN;
}

return TextType.UNKNOWN;
}

export default function BlidScanner({
onResult,
}: {
onResult: (scannedText: string) => Promise<void>;
}) {
const handleCodeDetection = async (
detectedCodes: IDetectedBarcode[],
): Promise<void> => {
const didFindBlid = detectedCodes.some(
(code) => determineScannedTextType(code.rawValue) === TextType.BLID,
);
const codesToProcess = didFindBlid
? detectedCodes.filter(
(code) => determineScannedTextType(code.rawValue) === TextType.BLID,
)
: detectedCodes;

for (const code of codesToProcess) {
try {
await onResult(code.rawValue);
} catch (error) {
console.error("Failed to handle scan", error);
}
// Arbitrary delay to somewhat avoid races the backend isn't smart enough to handle
await new Promise((resolve) => {
window.setTimeout(resolve, 250);
});
}
};

return (
<Scanner
constraints={{ facingMode: "environment" }}
formats={["qr_code", "code_128", "ean_8", "ean_13"]}
components={{ torch: true }}
onScan={handleCodeDetection}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@mui/material";
import React, { useState } from "react";

const ManualRegistrationModal = ({
const ManualBlidSearchModal = ({
open,
handleClose,
handleSubmit,
Expand Down Expand Up @@ -69,4 +69,4 @@ const ManualRegistrationModal = ({
);
};

export default ManualRegistrationModal;
export default ManualBlidSearchModal;
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { Close, InputRounded } from "@mui/icons-material";
import { AlertColor, Box, Button, Card, Modal, Stack } from "@mui/material";
import Typography from "@mui/material/Typography";
import { Scanner, IDetectedBarcode } from "@yudiel/react-qr-scanner";
import React, { useEffect, useState } from "react";
import React, { ReactNode, useState } from "react";

import { ItemStatus } from "@/components/matches/matches-helper";
import ProgressBar from "@/components/matches/matchesList/ProgressBar";
import MatchItemTable from "@/components/matches/MatchItemTable";
import ManualRegistrationModal from "@/components/matches/Scanner/ManualRegistrationModal";
import ScannerFeedback from "@/components/matches/Scanner/ScannerFeedback";
import { assertBlApiError, ScannedTextType, TextType } from "@/utils/types";

function determineScannedTextType(scannedText: string): ScannedTextType {
if (/^[\dA-Za-z]{12}$|^\d{8}$/.test(scannedText)) {
return TextType.BLID;
} else if (/^\d{13}$/.test(scannedText)) {
return TextType.ISBN;
}

return TextType.UNKNOWN;
}
import BlidScanner, {
determineScannedTextType,
} from "@/components/scanner/BlidScanner";
import ManualBlidSearchModal from "@/components/scanner/ManualBlidSearchModal";
import ScannerFeedback from "@/components/scanner/ScannerFeedback";
import { assertBlApiError, TextType } from "@/utils/types";

type Feedback = {
text: string;
Expand All @@ -32,17 +20,15 @@ const ScannerModal = ({
open,
handleClose,
handleSuccessfulScan,
itemStatuses,
expectedItems,
fulfilledItems,
allowManualRegistration,
children,
}: {
onScan: (blid: string) => Promise<[{ feedback: string }]>;
open: boolean;
handleClose: () => void;
handleSuccessfulScan?: (() => void) | undefined;
itemStatuses: ItemStatus[];
expectedItems: string[];
fulfilledItems: string[];
allowManualRegistration?: boolean;
children?: ReactNode;
}) => {
const [manualRegistrationModalOpen, setManualRegistrationModalOpen] =
useState(false);
Expand Down Expand Up @@ -96,46 +82,6 @@ const ScannerModal = ({
}
};

useEffect(() => {
if (
open &&
expectedItems.length === fulfilledItems.length &&
!(feedback.visible && feedback.severity === "info")
) {
handleClose();
}
}, [
expectedItems.length,
fulfilledItems.length,
handleClose,
open,
feedback.visible,
feedback.severity,
]);

const handleCodeDetection = async (
detectedCodes: IDetectedBarcode[],
): Promise<void> => {
const didFindBlid = detectedCodes.some(
(code) => determineScannedTextType(code.rawValue) === TextType.BLID,
);
const codesToProcess = didFindBlid
? detectedCodes.filter(
(code) => determineScannedTextType(code.rawValue) === TextType.BLID,
)
: detectedCodes;

for (const code of codesToProcess) {
await handleRegistration(code.rawValue).catch((error) =>
console.error("Failed to handle scan", error),
);
// Arbitrary delay to somewhat avoid races the backend isn't smart enough to handle
await new Promise((resolve) => {
window.setTimeout(resolve, 250);
});
}
};

return (
<Modal
open={open}
Expand All @@ -162,43 +108,20 @@ const ScannerModal = ({
flexShrink: 0,
}}
>
<Scanner
constraints={{ facingMode: "environment" }}
formats={["qr_code", "code_128", "ean_8", "ean_13"]}
components={{ torch: true }}
onScan={handleCodeDetection}
/>
</Box>
<Box width={0.9}>
<ProgressBar
percentComplete={
(fulfilledItems.length * 100) / expectedItems.length
}
subtitle={
<Typography textAlign={"center"}>
{fulfilledItems.length} av {expectedItems.length} bøker mottatt
</Typography>
}
/>
</Box>
<Box
sx={{
overflowY: "auto",
maxHeight: "30rem",
mt: 2,
}}
>
<MatchItemTable itemStatuses={itemStatuses} isSender={false} />
<BlidScanner onResult={handleRegistration} />
</Box>
{children}
<Stack direction={"row"} gap={1} mt={2}>
<Button
color={"info"}
variant={"outlined"}
startIcon={<InputRounded />}
onClick={() => setManualRegistrationModalOpen(true)}
>
Manuell registrering
</Button>
{allowManualRegistration && (
<Button
color={"info"}
variant={"outlined"}
startIcon={<InputRounded />}
onClick={() => setManualRegistrationModalOpen(true)}
>
Skriv inn blid manuelt
</Button>
)}
<Button
color={"info"}
startIcon={<Close />}
Expand All @@ -208,7 +131,7 @@ const ScannerModal = ({
Lukk
</Button>
</Stack>
<ManualRegistrationModal
<ManualBlidSearchModal
open={manualRegistrationModalOpen}
handleClose={() => {
setManualRegistrationModalOpen(false);
Expand Down
Loading

0 comments on commit 26370ea

Please sign in to comment.