From c469a7ff8106b5a6e957e2296977ed865a80afbd Mon Sep 17 00:00:00 2001 From: Adrian Andersen Date: Sun, 9 Jun 2024 13:54:10 +0200 Subject: [PATCH 1/4] feat(Match): update user match DTO --- package.json | 2 +- src/components/matches/matches-helper.tsx | 7 ++----- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 7888f82..1a01b4c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "validator": "^13.11.0" }, "devDependencies": { - "@boklisten/bl-model": "^0.25.37", + "@boklisten/bl-model": "^0.25.41", "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@testing-library/react": "^15.0.5", diff --git a/src/components/matches/matches-helper.tsx b/src/components/matches/matches-helper.tsx index 6e41c15..d79a0f3 100644 --- a/src/components/matches/matches-helper.tsx +++ b/src/components/matches/matches-helper.tsx @@ -38,11 +38,8 @@ export function calculateFulfilledUserMatchCustomerItems( isSender: boolean, ): string[] { return match.expectedItems.filter((item) => - (isSender - ? match.deliveredCustomerItems - : match.receivedCustomerItems - ).some( - (customerItem) => match.customerItemToItemMap[customerItem] === item, + (isSender ? match.deliveredBlIds : match.receivedBlIds).some( + (blId) => match.blIdToItemMap[blId] === item, ), ); } diff --git a/yarn.lock b/yarn.lock index 4c23a20..cc6ae69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -64,10 +64,10 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@boklisten/bl-model@^0.25.37": - version "0.25.37" - resolved "https://registry.yarnpkg.com/@boklisten/bl-model/-/bl-model-0.25.37.tgz#53cf9ea5b38bc953ada24b3d1c353707d83496fc" - integrity sha512-+KgdcMqD390D9HCQ/3x5uRpSJdSdbj1s/1mjEbmo6oy48IDwnS9qJbzUFXZ4V0G+4QmrG+cE6AJ3MCC8UNiiBA== +"@boklisten/bl-model@^0.25.41": + version "0.25.41" + resolved "https://registry.yarnpkg.com/@boklisten/bl-model/-/bl-model-0.25.41.tgz#81bc3c1d26ac8f91572f2f8c2f53c4a841f036bb" + integrity sha512-u7S5ap2ZjEZo5v+5hou5kmPqmurLP6gTcyNFQMl88yzI1bKOd0dJrfhYil1eDXDm12U7L1FkW1cDQUaezyL9Pg== dependencies: typescript "^5.2.2" From 6eaf6d5e903fec69037e4a8ce27c08402e423370 Mon Sep 17 00:00:00 2001 From: Adrian Andersen Date: Sun, 9 Jun 2024 21:39:55 +0200 Subject: [PATCH 2/4] feat(Scanner): complete redesign --- src/components/CountdownToRedirect.tsx | 48 +++++ src/components/matches/MatchDetail.tsx | 2 +- src/components/matches/MatchItemTable.tsx | 44 ++-- src/components/matches/OtherPersonContact.tsx | 8 +- .../Scanner/ManualRegistrationModal.tsx | 106 +++++---- src/components/matches/Scanner/Scanner.tsx | 156 -------------- .../matches/Scanner/ScannerFeedback.tsx | 48 +++-- .../matches/Scanner/ScannerModal.tsx | 204 ++++++++++++++++-- .../matches/Scanner/ScannerTutorial.tsx | 2 +- src/components/matches/StandMatchDetail.tsx | 12 +- src/components/matches/UserMatchDetail.tsx | 90 +++++--- 11 files changed, 410 insertions(+), 310 deletions(-) create mode 100644 src/components/CountdownToRedirect.tsx delete mode 100644 src/components/matches/Scanner/Scanner.tsx diff --git a/src/components/CountdownToRedirect.tsx b/src/components/CountdownToRedirect.tsx new file mode 100644 index 0000000..91b2789 --- /dev/null +++ b/src/components/CountdownToRedirect.tsx @@ -0,0 +1,48 @@ +import { LinearProgress, Box, Typography } from "@mui/material"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +const CountdownToRedirect = ({ + path, + seconds, +}: { + path: string; + seconds: number; +}) => { + const [progress, setProgress] = useState(100); + const router = useRouter(); + + useEffect(() => { + const interval = setInterval(() => { + setProgress((previousProgress) => { + if (previousProgress <= 0) { + clearInterval(interval); + router.push(path); + return 0; + } + return previousProgress - 10 / seconds; + }); + }, 100); + + return () => { + clearInterval(interval); + }; + }, [path, router, seconds]); + + return ( + + + Du blir videresendt om {Math.ceil((progress / 100) * seconds)}{" "} + sekunder... + + + + ); +}; + +export default CountdownToRedirect; diff --git a/src/components/matches/MatchDetail.tsx b/src/components/matches/MatchDetail.tsx index 878b980..cd70963 100644 --- a/src/components/matches/MatchDetail.tsx +++ b/src/components/matches/MatchDetail.tsx @@ -61,7 +61,7 @@ const MatchDetail = ({ matchId }: { matchId: string }) => { {match._variant === MatchVariant.StandMatch && ( - + )} {match._variant === MatchVariant.UserMatch && ( diff --git a/src/components/matches/MatchItemTable.tsx b/src/components/matches/MatchItemTable.tsx index b7a57ac..6fafa73 100644 --- a/src/components/matches/MatchItemTable.tsx +++ b/src/components/matches/MatchItemTable.tsx @@ -31,27 +31,29 @@ const MatchItemTable = ({ - {itemStatuses.map((item) => ( - - {item.title} - - - {item.fulfilled ? ( - - ) : ( - - )} - - - - ))} + {itemStatuses + .sort((a, b) => Number(a.fulfilled) - Number(b.fulfilled)) + .map((item) => ( + + {item.title} + + + {item.fulfilled ? ( + + ) : ( + + )} + + + + ))} diff --git a/src/components/matches/OtherPersonContact.tsx b/src/components/matches/OtherPersonContact.tsx index 207a0a8..152f2dd 100644 --- a/src/components/matches/OtherPersonContact.tsx +++ b/src/components/matches/OtherPersonContact.tsx @@ -1,21 +1,17 @@ -import { MatchVariant, MatchWithDetails } from "@boklisten/bl-model"; import PhoneIphoneIcon from "@mui/icons-material/PhoneIphone"; import { Box, Typography } from "@mui/material"; import React from "react"; import DynamicLink from "@/components/DynamicLink"; -import ContactInfo from "@/components/info/ContactInfo"; +import { UserMatchWithDetails } from "@/utils/types"; const OtherPersonContact = ({ match, currentUserId, }: { - match: MatchWithDetails; + match: UserMatchWithDetails; currentUserId: string; }) => { - if (match._variant === MatchVariant.StandMatch) { - return ; - } const otherPerson = match.receiver === currentUserId ? match.senderDetails diff --git a/src/components/matches/Scanner/ManualRegistrationModal.tsx b/src/components/matches/Scanner/ManualRegistrationModal.tsx index 0d7eade..2348b1e 100644 --- a/src/components/matches/Scanner/ManualRegistrationModal.tsx +++ b/src/components/matches/Scanner/ManualRegistrationModal.tsx @@ -1,11 +1,12 @@ -import { LoadingButton } from "@mui/lab"; +import { Close, InputRounded } from "@mui/icons-material"; import { - Box, + Alert, Button, - Container, + Dialog, + DialogActions, + DialogContent, InputLabel, - Modal, - Paper, + Stack, TextField, Typography, } from "@mui/material"; @@ -19,61 +20,52 @@ const ManualRegistrationModal = ({ open: boolean; handleClose: () => void; // eslint-disable-next-line no-unused-vars - handleSubmit: (scannedText: string) => Promise; + handleSubmit: (scannedText: string) => void; }) => { const [manualInput, setManualInput] = useState(""); - const [waiting, setWaiting] = useState(false); return ( - - - Manuell registrering - - Skriv inn bokas unike ID - - setManualInput(event.target.value)} - /> - - - { - setWaiting(true); - const success = await handleSubmit(manualInput); - setWaiting(false); - if (success) { - setManualInput(""); - } - }} - > - Bekreft - - - - + + + + Manuell registrering + + Skal kun brukes dersom bokas unike ID ikke lar seg skanne + + + Skriv inn bokas unike ID + + setManualInput(event.target.value)} + /> + + + + + + + ); }; diff --git a/src/components/matches/Scanner/Scanner.tsx b/src/components/matches/Scanner/Scanner.tsx deleted file mode 100644 index a843238..0000000 --- a/src/components/matches/Scanner/Scanner.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner"; -import { AlertColor, Button } from "@mui/material"; -import Box from "@mui/material/Box"; -import React, { useRef, useState } from "react"; - -import { addWithEndpoint } from "@/api/api"; -import ScannerFeedback from "@/components/matches/Scanner/ScannerFeedback"; -import ScannerModal from "@/components/matches/Scanner/ScannerModal"; -import ScannerTutorial from "@/components/matches/Scanner/ScannerTutorial"; -import { ScannedTextType, TextType } from "@/utils/types"; - -function determineScannedTextType(scannedText: string): ScannedTextType { - if (Number.isNaN(Number(scannedText))) { - if (scannedText.length === 12) { - return TextType.BLID; - } - } else { - if (scannedText.length === 8) { - return TextType.BLID; - } else if (scannedText.length === 13) { - return TextType.ISBN; - } - } - return TextType.UNKNOWN; -} - -type Feedback = { - text: string; - severity: AlertColor; - visible: boolean; -}; - -const Scanner = ({ forceUpdate }: { forceUpdate: () => void }) => { - const [scanModalOpen, setScanModalOpen] = useState(false); - /* - const [manualRegistrationModalOpen, setManualRegistrationModalOpen] = - useState(false); - */ - - const [feedback, setFeedback] = useState({ - text: "", - severity: "success", - visible: false, - }); - const scannerLocked = useRef(false); - - const displayFeedback = (text: string, severity: AlertColor) => { - setFeedback({ text, severity, visible: true }); - }; - - const handleRegistration = async (scannedText: string): Promise => { - const scannedTextType = determineScannedTextType(scannedText); - - if (scannedTextType === TextType.ISBN) { - displayFeedback( - "Feil strekkode. Bruk bokas unike ID. Se instruksjoner for hjelp", - "error", - ); - return false; - } - - if (scannedTextType === TextType.UNKNOWN) { - displayFeedback( - "Ugyldig strekkode. Vennligst prøv igjen, eller ta kontakt med stand for hjelp", - "error", - ); - return false; - } - - if (scannerLocked.current) { - return false; - } - scannerLocked.current = true; - try { - const response = await addWithEndpoint( - "matches", - "transfer-item", - JSON.stringify({ blid: scannedText }), - ); - navigator.vibrate(100); - const feedback = response.data?.data?.[0]?.feedback; - displayFeedback( - feedback ?? "Boken har blitt registrert!", - feedback ? "info" : "success", - ); - // setManualRegistrationModalOpen(false); - setScanModalOpen(false); - return true; - } catch (error) { - displayFeedback(String(error), "error"); - return false; - } finally { - scannerLocked.current = false; - // TODO: test with serveo, do we need forceUpdate? - forceUpdate(); - } - }; - - return ( - - - - setFeedback((previous) => ({ ...previous, visible: false })) - } - /> - - {/** - * -

eller

- - { - setManualRegistrationModalOpen(false); - setFeedbackVisible(false); - }} - handleSubmit={handleRegistration} - /> - * - */} - { - setScanModalOpen(false); - setFeedback((previous) => ({ ...previous, visible: false })); - }} - handleSubmit={handleRegistration} - /> -
- ); -}; - -export default Scanner; diff --git a/src/components/matches/Scanner/ScannerFeedback.tsx b/src/components/matches/Scanner/ScannerFeedback.tsx index 460b7df..3c35767 100644 --- a/src/components/matches/Scanner/ScannerFeedback.tsx +++ b/src/components/matches/Scanner/ScannerFeedback.tsx @@ -1,4 +1,13 @@ -import { Alert, AlertColor, Snackbar } from "@mui/material"; +import { + Alert, + AlertColor, + Button, + Dialog, + DialogActions, + DialogContent, + Snackbar, +} from "@mui/material"; +import Typography from "@mui/material/Typography"; import React from "react"; const ScannerFeedback = ({ @@ -16,7 +25,7 @@ const ScannerFeedback = ({ _event?: React.SyntheticEvent | Event, reason?: string, ) => { - if (reason === "clickaway") { + if (reason === "clickaway" || severity === "info") { return; } @@ -24,17 +33,30 @@ const ScannerFeedback = ({ }; return ( - - {feedback} - + <> + + + {feedback} + + + + + Viktig informasjon + {feedback} + + + + + + ); }; diff --git a/src/components/matches/Scanner/ScannerModal.tsx b/src/components/matches/Scanner/ScannerModal.tsx index 4aad203..8bcb4d7 100644 --- a/src/components/matches/Scanner/ScannerModal.tsx +++ b/src/components/matches/Scanner/ScannerModal.tsx @@ -1,50 +1,214 @@ -import { Box, Button, Container, Modal } from "@mui/material"; +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 } from "@yudiel/react-qr-scanner"; -import React from "react"; +import React, { useEffect, useState } from "react"; + +import { addWithEndpoint } from "@/api/api"; +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 { ScannedTextType, TextType } from "@/utils/types"; + +function determineScannedTextType(scannedText: string): ScannedTextType { + if (Number.isNaN(Number(scannedText))) { + if (scannedText.length === 12) { + return TextType.BLID; + } + } else { + if (scannedText.length === 8) { + return TextType.BLID; + } else if (scannedText.length === 13) { + return TextType.ISBN; + } + } + return TextType.UNKNOWN; +} + +type Feedback = { + text: string; + severity: AlertColor; + visible: boolean; +}; const ScannerModal = ({ open, handleClose, - handleSubmit, + itemStatuses, + expectedItems, + fulfilledItems, }: { open: boolean; handleClose: () => void; - // eslint-disable-next-line no-unused-vars - handleSubmit: (scannedText: string) => Promise; + itemStatuses: ItemStatus[]; + expectedItems: string[]; + fulfilledItems: string[]; }) => { - console.log("scannermodal"); + const [manualRegistrationModalOpen, setManualRegistrationModalOpen] = + useState(false); + + const [feedback, setFeedback] = useState({ + text: "", + severity: "success", + visible: false, + }); + + const handleRegistration = async (scannedText: string) => { + const scannedTextType = determineScannedTextType(scannedText); + if (scannedTextType === TextType.ISBN) { + setFeedback({ + text: "Feil strekkode. Bruk bokas unike ID. Se instruksjoner for hjelp", + severity: "error", + visible: true, + }); + return; + } + if (scannedTextType === TextType.UNKNOWN) { + setFeedback({ + text: "Ugyldig strekkode. Vennligst prøv igjen, eller ta kontakt med stand for hjelp", + severity: "error", + visible: true, + }); + return; + } + + try { + const response = await addWithEndpoint( + "matches", + "transfer-item", + JSON.stringify({ blid: scannedText }), + ); + try { + navigator?.vibrate(100); + } catch { + // Some browsers or devices may not have implemented the vibrate function + } + const feedback = response.data?.data?.[0]?.feedback; + setFeedback({ + text: feedback ?? "Boken har blitt registrert!", + severity: feedback ? "info" : "success", + visible: true, + }); + } catch (error) { + setFeedback({ + text: String(error), + severity: "error", + visible: true, + }); + } + }; + + useEffect(() => { + if (open && expectedItems.length === fulfilledItems.length) { + handleClose(); + } + }, [expectedItems.length, fulfilledItems.length, handleClose, open]); + return ( - - + - - { for (const code of detectedCodes) { - handleSubmit(code.rawValue); + handleRegistration(code.rawValue); } }} /> - + + + {fulfilledItems.length} av {expectedItems.length} bøker mottatt + + } + /> + + + + + + + + + { + setManualRegistrationModalOpen(false); + }} + handleSubmit={(scannedText) => { + setManualRegistrationModalOpen(false); + handleRegistration(scannedText); + }} + /> + + setFeedback((previous) => ({ ...previous, visible: false })) + } + /> + ); }; diff --git a/src/components/matches/Scanner/ScannerTutorial.tsx b/src/components/matches/Scanner/ScannerTutorial.tsx index c8832aa..4f5d6d3 100644 --- a/src/components/matches/Scanner/ScannerTutorial.tsx +++ b/src/components/matches/Scanner/ScannerTutorial.tsx @@ -54,7 +54,7 @@ const ScannerTutorial = () => { }} > - 1. Scan eller skriv inn en boks unike ID, som ser slik ut: + 1. Scan eller skriv inn en bok sin unike ID, som ser slik ut: { +const StandMatchDetail = ({ match }: { match: StandMatchWithDetails }) => { const { fulfilledHandoffItems, fulfilledPickupItems } = calculateFulfilledStandMatchItems(match); const isFulfilled = @@ -106,9 +99,6 @@ const StandMatchDetail = ({ Du skal på stand: - - Kontaktinformasjon - ); }; diff --git a/src/components/matches/UserMatchDetail.tsx b/src/components/matches/UserMatchDetail.tsx index c6a4e0b..b7c7235 100644 --- a/src/components/matches/UserMatchDetail.tsx +++ b/src/components/matches/UserMatchDetail.tsx @@ -1,6 +1,8 @@ -import { Alert, Box, Typography } from "@mui/material"; -import React, { useCallback, useState } from "react"; +import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner"; +import { Alert, Box, Button, Typography } from "@mui/material"; +import React, { useState } from "react"; +import CountdownToRedirect from "@/components/CountdownToRedirect"; import { calculateFulfilledUserMatchCustomerItems, calculateItemStatuses, @@ -12,7 +14,8 @@ import ProgressBar from "@/components/matches/matchesList/ProgressBar"; import MatchItemTable from "@/components/matches/MatchItemTable"; import MeetingInfo from "@/components/matches/MeetingInfo"; import OtherPersonContact from "@/components/matches/OtherPersonContact"; -import Scanner from "@/components/matches/Scanner/Scanner"; +import ScannerModal from "@/components/matches/Scanner/ScannerModal"; +import ScannerTutorial from "@/components/matches/Scanner/ScannerTutorial"; import { UserMatchWithDetails } from "@/utils/types"; const UserMatchDetail = ({ @@ -22,8 +25,9 @@ const UserMatchDetail = ({ match: UserMatchWithDetails; currentUserId: string; }) => { - const [, updateState] = useState({}); - const forceUpdate = useCallback(() => updateState({}), []); + const [scanModalOpen, setScanModalOpen] = useState(false); + const [redirectCountdownStarted, setRedirectCountdownStarted] = + useState(false); const isSender = match.sender === currentUserId; const fulfilledItems = calculateFulfilledUserMatchCustomerItems( match, @@ -54,10 +58,15 @@ const UserMatchDetail = ({ {isFulfilled && ( - - Du har {isSender ? "levert" : "mottatt"} alle bøkene for denne - overleveringen. - + + + Du har {isSender ? "levert" : "mottatt"} alle bøkene for denne + overleveringen. + + {redirectCountdownStarted && ( + + )} + )} {fulfilledItems.length !== otherPersonFulfilledItems.length && isSender && ( @@ -79,29 +88,62 @@ const UserMatchDetail = ({ } /> - - Hvordan fungerer det? - - Du skal møte en annen elev og utveksle bøker. Det er viktig at den som - mottar bøker scanner hver bok, hvis ikke blir ikke bøkene registrert - som levert, og avsender kan få faktura. - - - Du skal møte - - + {!isFulfilled && ( + <> + + Hvordan fungerer det? + + Du skal møte en annen elev og utveksle bøker. Det er viktig at den + som mottar bøker scanner hver bok, hvis ikke blir ikke bøkene + registrert som levert, og avsender kan få faktura. + + + Du skal møte + + + + )} {!isSender && !isFulfilled && ( <> Når du skal motta bøkene - + + + + )} - - Du skal {isSender ? "levere" : "motta"} disse bøkene - + {!isFulfilled && ( + + Du skal {isSender ? "levere" : "motta"} disse bøkene + + )} + + { + setScanModalOpen(false); + setRedirectCountdownStarted(isFulfilled); + }} + itemStatuses={itemStatuses} + expectedItems={match.expectedItems} + fulfilledItems={fulfilledItems} + /> ); }; From 5df188131576cc4deb53a34941cbf49bd4738ffd Mon Sep 17 00:00:00 2001 From: Adrian Andersen Date: Sun, 9 Jun 2024 22:02:38 +0200 Subject: [PATCH 3/4] fix(Scanner): dynamic view height for stupid mobile browsers --- src/components/matches/Scanner/ScannerModal.tsx | 2 +- src/globals.css | 3 +++ src/pages/_app.tsx | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/globals.css diff --git a/src/components/matches/Scanner/ScannerModal.tsx b/src/components/matches/Scanner/ScannerModal.tsx index 8bcb4d7..1a85a9f 100644 --- a/src/components/matches/Scanner/ScannerModal.tsx +++ b/src/components/matches/Scanner/ScannerModal.tsx @@ -118,7 +118,7 @@ const ScannerModal = ({ flexDirection: "column", flexBasis: "30rem", maxWidth: "50rem", - maxHeight: "100vh", + maxHeight: "calc(var(--vh, 1vh) * 100)", paddingBottom: "1rem", borderRadius: 0, overflowY: "hidden", diff --git a/src/globals.css b/src/globals.css new file mode 100644 index 0000000..72e3d3d --- /dev/null +++ b/src/globals.css @@ -0,0 +1,3 @@ +:root { + --vh: 100%; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 9ca94cc..ae50359 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -22,6 +22,7 @@ import "@fontsource/roboto/300.css"; import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; +import "@/globals.css"; class OverriddenAdapter extends DateAdapter { // Get years in descending order @@ -55,6 +56,19 @@ export default function MyApp(props: AppProps) { } }, [router]); + // Dynamic height variable to fix stupid mobile browsers + useEffect(() => { + function setDynamicHeight() { + const vh = window.innerHeight * 0.01; + document.documentElement.style.setProperty("--vh", `${vh}px`); + } + setDynamicHeight(); + window.addEventListener("resize", setDynamicHeight); + return () => { + window.removeEventListener("resize", setDynamicHeight); + }; + }, []); + return ( <> From 8b5cf17cb2112f068e93dfb087181ca2113494b8 Mon Sep 17 00:00:00 2001 From: Adrian Andersen Date: Mon, 10 Jun 2024 19:02:16 +0200 Subject: [PATCH 4/4] fix(Scanner): only display itemStatuses once --- src/components/matches/Scanner/ScannerModal.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/matches/Scanner/ScannerModal.tsx b/src/components/matches/Scanner/ScannerModal.tsx index 1a85a9f..fc834d7 100644 --- a/src/components/matches/Scanner/ScannerModal.tsx +++ b/src/components/matches/Scanner/ScannerModal.tsx @@ -162,15 +162,7 @@ const ScannerModal = ({ mt: "1rem", }} > - +