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/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
-
-
-
-
+
);
};
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 }))
- }
- />
- }
- variant={"contained"}
- onClick={() => setScanModalOpen(true)}
- >
- Scan bøker
-
- {/**
- *
- eller
- }
- variant={"contained"}
- onClick={() => setManualRegistrationModalOpen(true)}
- >
- Manuell registrering
-
- {
- 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}
+
+
+
+ >
);
};
diff --git a/src/components/matches/Scanner/ScannerModal.tsx b/src/components/matches/Scanner/ScannerModal.tsx
index 4aad203..fc834d7 100644
--- a/src/components/matches/Scanner/ScannerModal.tsx
+++ b/src/components/matches/Scanner/ScannerModal.tsx
@@ -1,50 +1,206 @@
-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
+
+ }
+ />
+
+
+
+
+
+ }
+ onClick={() => setManualRegistrationModalOpen(true)}
+ >
+ Manuell registrering
+
+ }
+ variant={"contained"}
+ onClick={handleClose}
+ >
+ Lukk
+
+
+ {
+ 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
-
+
+
+ }
+ variant={"contained"}
+ onClick={() => setScanModalOpen(true)}
+ >
+ Scan bøker
+
+
>
)}
-
- 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}
+ />
>
);
};
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/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 (
<>
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"