move to git.n39.euall books in the bag ...
This commit is contained in:
commit
8cc2662092
64 changed files with 134252 additions and 0 deletions
frontend/src/shared/components/modals
184
frontend/src/shared/components/modals/AuthenticationModal.tsx
Normal file
184
frontend/src/shared/components/modals/AuthenticationModal.tsx
Normal file
|
@ -0,0 +1,184 @@
|
|||
import { useCallback, useContext } from "react";
|
||||
import { Button, Col, Form, Modal, Row } from "react-bootstrap";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { TfiKey } from "react-icons/tfi";
|
||||
import { primaryRGBA, primary, secondary } from "../../../colors";
|
||||
import { ModalHeader } from "./ModalHeader";
|
||||
import { AuthContext } from "../../../App";
|
||||
import { AuthModalProps } from "./types";
|
||||
|
||||
export const AuthenticationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
isUpdate,
|
||||
}: Omit<AuthModalProps, "type">) => {
|
||||
const { control, register, formState, setError, reset } = useForm<{
|
||||
password: string;
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}>({
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const values = useWatch({ control });
|
||||
|
||||
const auth = useContext(AuthContext);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (
|
||||
data: Partial<{
|
||||
password: string;
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}>
|
||||
) => {
|
||||
if (!data.password && !isUpdate) {
|
||||
return setError("password", { message: "Password is required." });
|
||||
}
|
||||
if (isUpdate && !data.oldPassword) {
|
||||
return setError("oldPassword", {
|
||||
message: "Current password is required.",
|
||||
});
|
||||
}
|
||||
if (isUpdate && !data.newPassword) {
|
||||
return setError("newPassword", {
|
||||
message: "New password is required.",
|
||||
});
|
||||
}
|
||||
const res = await fetch(
|
||||
isUpdate ? "/api/updateAdminKey" : "/api/authenticate",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
setError(isUpdate ? "newPassword" : "password", {
|
||||
message: await res.text(),
|
||||
});
|
||||
reset();
|
||||
} else {
|
||||
auth.setAuthenticated?.(true);
|
||||
reset();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isUpdate, setError, reset, auth, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={open}
|
||||
onHide={onClose}
|
||||
style={{ backgroundColor: primaryRGBA }}
|
||||
centered
|
||||
>
|
||||
<ModalHeader
|
||||
onClose={onClose}
|
||||
title={isUpdate ? "Set new Admin key" : "Admin Login"}
|
||||
icon={<TfiKey size={50} className='ml-0 mr-auto' />}
|
||||
/>
|
||||
<Modal.Body
|
||||
style={{
|
||||
border: "2px solid black",
|
||||
borderTop: "none",
|
||||
backgroundColor: primary,
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
className='mb-2'
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
onSubmit(values);
|
||||
}}
|
||||
>
|
||||
{isUpdate && (
|
||||
<>
|
||||
<Form.Group as={Row} className='mb-2'>
|
||||
<Col sm='4'>
|
||||
<Form.Label>Current Password</Form.Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Control
|
||||
{...register("oldPassword", { required: true })}
|
||||
type='password'
|
||||
placeholder='Enter current admin password ...'
|
||||
isInvalid={!!formState.errors.oldPassword}
|
||||
/>
|
||||
<Form.Control.Feedback type='invalid'>
|
||||
{!values.oldPassword
|
||||
? "Old password is required"
|
||||
: formState.errors.oldPassword?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group as={Row} className='mb-2'>
|
||||
<Col sm='4'>
|
||||
<Form.Label>New Password</Form.Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Control
|
||||
{...register("newPassword", {
|
||||
required: true,
|
||||
})}
|
||||
type='password'
|
||||
placeholder={`Enter new admin password ...`}
|
||||
isInvalid={!!formState.errors.newPassword}
|
||||
/>
|
||||
<Form.Control.Feedback type='invalid'>
|
||||
{!values.newPassword
|
||||
? "New password is required"
|
||||
: formState.errors.newPassword?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>{" "}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isUpdate && (
|
||||
<Form.Group as={Row} className='mb-2'>
|
||||
<Col sm='4'>
|
||||
<Form.Label>Password</Form.Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Control
|
||||
{...register("password", {
|
||||
required: true,
|
||||
})}
|
||||
type='password'
|
||||
placeholder={`Enter admin password ...`}
|
||||
isInvalid={!!formState.errors.password}
|
||||
/>
|
||||
<Form.Control.Feedback type='invalid'>
|
||||
{!values.password
|
||||
? "Password is required"
|
||||
: formState.errors.password?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
<div className='d-flex mx-auto mb-auto mt-2 w-100'>
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
marginLeft: "auto",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
onClick={() => onSubmit(values)}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
268
frontend/src/shared/components/modals/BookModal.tsx
Normal file
268
frontend/src/shared/components/modals/BookModal.tsx
Normal file
|
@ -0,0 +1,268 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Col, Form, Modal, Row } from "react-bootstrap";
|
||||
import { ImBook, ImCamera } from "react-icons/im";
|
||||
import { ModalHeader } from "./ModalHeader";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { Book, bookShelfs } from "../../../types/Book";
|
||||
import { useScanner } from "../../utils/useScanner";
|
||||
import { primary, primaryRGBA, secondary } from "../../../colors";
|
||||
import { BookModalProps } from "./types";
|
||||
|
||||
type BookForm = Pick<Book, "isbn" | "title" | "shelf" | "published">;
|
||||
|
||||
export const BookModal = ({
|
||||
open,
|
||||
onClose,
|
||||
book,
|
||||
}: Omit<BookModalProps, "type">): React.JSX.Element => {
|
||||
const isEdit = !!book;
|
||||
const [showScanner, setShowScanner] = useState(false);
|
||||
|
||||
const { control, register, formState, setError, setValue, reset } =
|
||||
useForm<BookForm>({
|
||||
mode: "onChange",
|
||||
defaultValues: book,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(book);
|
||||
}, [book, reset]);
|
||||
|
||||
const { scannerError, setScannerRef } = useScanner({
|
||||
onDetected: (result) => {
|
||||
setValue("isbn", result);
|
||||
setShowScanner(false);
|
||||
},
|
||||
});
|
||||
|
||||
const values = useWatch({ control });
|
||||
|
||||
const [submitError, setSubmitError] = useState<string | undefined>();
|
||||
const onSubmit = useCallback(
|
||||
async (data: Partial<BookForm>) => {
|
||||
setSubmitError(undefined);
|
||||
let error = false;
|
||||
if (!data.isbn?.length) {
|
||||
setError("isbn", { message: "ISBN is required" });
|
||||
error = true;
|
||||
}
|
||||
if (!data.title?.length) {
|
||||
setError("title", { message: "Title is required" });
|
||||
error = true;
|
||||
}
|
||||
if (!data.shelf?.length) {
|
||||
setError("shelf", { message: "Shelf is required" });
|
||||
error = true;
|
||||
}
|
||||
if (!data.published) {
|
||||
setError("published", { message: "Year published is required" });
|
||||
error = true;
|
||||
}
|
||||
if (error) return;
|
||||
const createdBook = await fetch(
|
||||
isEdit ? "api/books/edit" : "/api/books/create",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!createdBook.ok) {
|
||||
setSubmitError(await createdBook.text());
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isEdit, onClose, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={open}
|
||||
onHide={onClose}
|
||||
style={{ backgroundColor: primaryRGBA }}
|
||||
centered
|
||||
>
|
||||
<ModalHeader
|
||||
onClose={onClose}
|
||||
title={`${!!book ? "Edit" : "Add new"} Book`}
|
||||
icon={<ImBook size={50} className="ml-0 mr-auto" />}
|
||||
/>
|
||||
<Modal.Body
|
||||
style={{
|
||||
border: "2px solid black",
|
||||
borderTop: "none",
|
||||
backgroundColor: primary,
|
||||
}}
|
||||
>
|
||||
{!showScanner && (
|
||||
<Form
|
||||
className="mb-2"
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
onSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Group as={Row} className="mb-2">
|
||||
<Col sm="2">
|
||||
<Form.Label>ISBN</Form.Label>
|
||||
</Col>
|
||||
<Col className="d-flex flex-column">
|
||||
<div className="d-flex">
|
||||
<Form.Control
|
||||
{...register("isbn", { required: true, maxLength: 360 })}
|
||||
isInvalid={!!formState.errors.isbn}
|
||||
style={{
|
||||
borderRadius: "5px 0px 0px 5px",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: "0px 5px 5px 0px",
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
}}
|
||||
className="mr-2 pt-0"
|
||||
disabled={showScanner}
|
||||
onClick={() => setShowScanner(true)}
|
||||
>
|
||||
<ImCamera size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<Form.Control.Feedback
|
||||
style={{ display: !formState.errors.isbn ? "none" : "block" }}
|
||||
type="invalid"
|
||||
>
|
||||
{!formState.errors.isbn
|
||||
? "ISBN is required"
|
||||
: formState.errors.isbn?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group as={Row} className="mb-2">
|
||||
<Col sm="2">
|
||||
<Form.Label>Title</Form.Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Control
|
||||
{...register("title", { required: true, maxLength: 360 })}
|
||||
isInvalid={!!formState.errors.title}
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{!values.title
|
||||
? "Title is required"
|
||||
: formState.errors.title?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group as={Row} className="mb-2">
|
||||
<Col sm="2">
|
||||
<Form.Label>Year published</Form.Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Control
|
||||
{...register("published", {
|
||||
required: true,
|
||||
maxLength: 360,
|
||||
validate: {
|
||||
isReasonable: (value) => {
|
||||
if (value > 1900 && value < 2150) {
|
||||
return true;
|
||||
}
|
||||
return `${value} is not a reasonable year`;
|
||||
},
|
||||
isNumber: (value) => {
|
||||
if (value && value.toString().match(/^[0-9\b]+$/)) {
|
||||
return true;
|
||||
}
|
||||
return "Please enter a valid year.";
|
||||
},
|
||||
},
|
||||
})}
|
||||
isInvalid={!!formState.errors.published}
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{!values.published
|
||||
? "Year published is required"
|
||||
: formState.errors.published?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group as={Row} className="mb-2">
|
||||
<Col sm="2">
|
||||
<Form.Label>Shelf</Form.Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Select
|
||||
{...register("shelf", { required: true })}
|
||||
isInvalid={!!formState.errors.shelf}
|
||||
>
|
||||
{Object.keys(bookShelfs).map((key) => (
|
||||
<option
|
||||
key="key"
|
||||
value={bookShelfs[key as keyof typeof bookShelfs]}
|
||||
>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{!values.shelf
|
||||
? "Shelf is required"
|
||||
: formState.errors.shelf?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<div className="d-flex mx-auto mb-auto mt-2 w-100">
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
marginLeft: "auto",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
onClick={() => onSubmit(values)}
|
||||
>
|
||||
{isEdit ? "Submit" : "Add Book"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
{showScanner && (
|
||||
<div
|
||||
className="w-100 overflow-hidden"
|
||||
ref={(ref) => setScannerRef(ref)}
|
||||
style={{ position: "relative", height: "25vh" }}
|
||||
>
|
||||
<canvas
|
||||
className="drawingBuffer w-100 position-absolute"
|
||||
style={{
|
||||
height: "100%",
|
||||
border: "2px solid black",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{scannerError ? (
|
||||
<p>
|
||||
ERROR INITIALIZING CAMERA ${JSON.stringify(scannerError)} -- DO YOU
|
||||
HAVE PERMISSION?
|
||||
</p>
|
||||
) : null}
|
||||
{!!submitError && (
|
||||
<Form.Control.Feedback style={{ display: "block" }} type="invalid">
|
||||
{submitError}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
196
frontend/src/shared/components/modals/CheckoutModal.tsx
Normal file
196
frontend/src/shared/components/modals/CheckoutModal.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { Alert, Button, Col, Form, Modal, Row } from "react-bootstrap";
|
||||
import { ImBoxAdd, ImBoxRemove } from "react-icons/im";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { Book } from "../../../types/Book";
|
||||
import { primary, primaryRGBA, secondary } from "../../../colors";
|
||||
import { ModalHeader } from "./ModalHeader";
|
||||
import { CheckoutBookModalProps } from "./types";
|
||||
import { AiOutlineExclamationCircle } from "react-icons/ai";
|
||||
|
||||
type BookCheckoutForm = Pick<Book, "checkoutBy" | "contact">;
|
||||
|
||||
export const CheckoutBookModal = ({
|
||||
uuid,
|
||||
title,
|
||||
open,
|
||||
onClose,
|
||||
checkoutBy,
|
||||
contact,
|
||||
}: Omit<CheckoutBookModalProps, "type">): React.JSX.Element => {
|
||||
const isChechout = !checkoutBy && !contact;
|
||||
|
||||
const [failed, setFailed] = useState<string>();
|
||||
|
||||
const { control, register, formState, setError } = useForm<BookCheckoutForm>({
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const values = useWatch({ control });
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: Partial<BookCheckoutForm>) => {
|
||||
if (!data.checkoutBy) {
|
||||
setError("checkoutBy", {
|
||||
message: `please enter ${
|
||||
isChechout ? "your" : "the"
|
||||
} name or nickname ${isChechout ? "" : "you prvided on checkout"}`,
|
||||
});
|
||||
}
|
||||
if (!data.contact) {
|
||||
setError("contact", {
|
||||
message: `please enter ${
|
||||
isChechout ? "some" : "the"
|
||||
} contact information ${isChechout ? "" : "you prvided on checkout"}`,
|
||||
});
|
||||
}
|
||||
const res = await fetch(
|
||||
isChechout ? "/api/books/checkout" : "/api/books/return",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ...data, uuid }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
onClose();
|
||||
} else {
|
||||
setFailed(await res.text());
|
||||
}
|
||||
},
|
||||
[uuid, onClose, setError, isChechout]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={open}
|
||||
onHide={onClose}
|
||||
style={{ backgroundColor: primaryRGBA }}
|
||||
centered
|
||||
>
|
||||
<ModalHeader
|
||||
onClose={onClose}
|
||||
title={`${isChechout ? "Checkout" : "Return"} book '${title}'`}
|
||||
icon={
|
||||
isChechout ? (
|
||||
<ImBoxRemove size={50} className='ml-0 mr-auto' />
|
||||
) : (
|
||||
<ImBoxAdd size={50} className='ml-0 mr-auto' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Modal.Body
|
||||
style={{
|
||||
border: "2px solid black",
|
||||
borderTop: "none",
|
||||
backgroundColor: primary,
|
||||
}}
|
||||
>
|
||||
<Alert className='w-80 m-auto my-4' variant='warning'>
|
||||
<AiOutlineExclamationCircle />
|
||||
Your contact information will only be visible to the admins and is
|
||||
deleted once you have returned this book.
|
||||
</Alert>
|
||||
<Form
|
||||
className='mb-2'
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
onSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Group as={Row} className='mb-2'>
|
||||
<Col sm='2'>
|
||||
<Form.Label>Name</Form.Label>
|
||||
</Col>
|
||||
<Col className='d-flex flex-column'>
|
||||
<Form.Control
|
||||
{...register("checkoutBy", {
|
||||
required: true,
|
||||
validate: {
|
||||
notNullString: (value) => {
|
||||
return (
|
||||
value?.toLocaleLowerCase() !== "null" ||
|
||||
"'null' is not a valid nickname"
|
||||
);
|
||||
},
|
||||
},
|
||||
})}
|
||||
isInvalid={!!formState.errors.checkoutBy}
|
||||
placeholder={isChechout ? "name or nickname" : checkoutBy ?? ""}
|
||||
style={{
|
||||
borderRadius: "5px 0px 0px 5px",
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type='invalid'>
|
||||
{!values.checkoutBy
|
||||
? "name is required"
|
||||
: formState.errors.checkoutBy?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group as={Row} className='mb-2'>
|
||||
<Col sm='2'>
|
||||
<Form.Label>Contact Info</Form.Label>
|
||||
</Col>
|
||||
<Col className='d-flex flex-column'>
|
||||
<Form.Control
|
||||
{...register("contact", {
|
||||
required: true,
|
||||
validate: {
|
||||
//function to test if contact is a valid email or phone number
|
||||
validContact: (value) => {
|
||||
if (
|
||||
value &&
|
||||
(value.match(/^[0-9]{11}$/) ||
|
||||
value.match(
|
||||
/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/
|
||||
))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return "email or phone number is required";
|
||||
},
|
||||
},
|
||||
})}
|
||||
placeholder={
|
||||
isChechout ? "email or phone number" : contact ?? ""
|
||||
}
|
||||
isInvalid={!!formState.errors.contact}
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type='invalid'>
|
||||
{!values.contact
|
||||
? "Some contact information is required"
|
||||
: formState.errors.contact?.message}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Control.Feedback
|
||||
style={{ display: !failed ? "none" : "flex" }}
|
||||
type='invalid'
|
||||
>
|
||||
<p className='mr-0'>{failed}</p>
|
||||
</Form.Control.Feedback>
|
||||
<div className='d-flex mx-auto mb-auto mt-2 w-100'>
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
marginLeft: "auto",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
onClick={() => onSubmit(values)}
|
||||
>
|
||||
{`${isChechout ? "Checkout" : "Return"} book`}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
76
frontend/src/shared/components/modals/DeleteBookModal.tsx
Normal file
76
frontend/src/shared/components/modals/DeleteBookModal.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { Button, Modal } from "react-bootstrap";
|
||||
import { primary, primaryRGBA, secondary } from "../../../colors";
|
||||
import { ModalHeader } from "./ModalHeader";
|
||||
import { ImBin } from "react-icons/im";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { DeleteBookModalProps } from "./types";
|
||||
|
||||
export const DeleteBookModal = ({
|
||||
uuid,
|
||||
open,
|
||||
onClose,
|
||||
}: Omit<DeleteBookModalProps, "type">): React.JSX.Element => {
|
||||
const { mutate: rm } = useMutation({
|
||||
mutationFn: async () => {
|
||||
await fetch(`/api/books/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ uuid }),
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={open}
|
||||
onHide={onClose}
|
||||
style={{ backgroundColor: primaryRGBA }}
|
||||
centered
|
||||
>
|
||||
<ModalHeader
|
||||
onClose={onClose}
|
||||
title={"Move to Shelf"}
|
||||
icon={<ImBin size={50} className='ml-0 mr-auto' />}
|
||||
/>
|
||||
<Modal.Body
|
||||
style={{
|
||||
border: "2px solid black",
|
||||
borderTop: "none",
|
||||
backgroundColor: primary,
|
||||
}}
|
||||
>
|
||||
<div className='d-flex mx-auto mb-auto mt-2 w-100'>
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
marginLeft: "auto",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
marginLeft: "auto",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
onClick={() => rm()}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
50
frontend/src/shared/components/modals/ModalHeader.tsx
Normal file
50
frontend/src/shared/components/modals/ModalHeader.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React from "react";
|
||||
import { Button, Modal } from "react-bootstrap";
|
||||
import { AiOutlineClose } from "react-icons/ai";
|
||||
import { primary, secondary } from "../../../colors";
|
||||
|
||||
export const ModalHeader = ({
|
||||
title,
|
||||
icon,
|
||||
onClose,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
title: JSX.Element | string;
|
||||
icon?: JSX.Element;
|
||||
}): React.JSX.Element => {
|
||||
return (
|
||||
<Modal.Header
|
||||
style={{
|
||||
border: "2px solid black",
|
||||
borderBottom: "none",
|
||||
backgroundColor: primary,
|
||||
}}
|
||||
>
|
||||
<Modal.Title className='w-100 d-flex' style={{ height: "fit-content" }}>
|
||||
{typeof title === "string" ? (
|
||||
<>
|
||||
{icon}
|
||||
<h2 className='m-auto p-2' style={{ textAlign: "center" }}>
|
||||
{title}
|
||||
</h2>
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<Button
|
||||
className='ml-auto mr-0'
|
||||
style={{
|
||||
backgroundColor: secondary,
|
||||
color: "black",
|
||||
border: "2px solid black",
|
||||
width: "fit-content",
|
||||
height: "fit-content",
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<AiOutlineClose />
|
||||
</Button>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
);
|
||||
};
|
54
frontend/src/shared/components/modals/Modals.tsx
Normal file
54
frontend/src/shared/components/modals/Modals.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import { AuthenticationModal } from "./AuthenticationModal";
|
||||
import { modalTypes } from "./types";
|
||||
import { BookModal } from "./BookModal";
|
||||
import { CheckoutBookModal } from "./CheckoutModal";
|
||||
import { ScannerModal } from "./ScannerModal";
|
||||
import { DeleteBookModal } from "./DeleteBookModal";
|
||||
import { ModalContextType } from "../../../App";
|
||||
|
||||
const { auth, book, checkout, scanner, del } = modalTypes;
|
||||
|
||||
export const ModalSelector = ({
|
||||
activeModal,
|
||||
setActiveModal,
|
||||
}: ModalContextType): React.JSX.Element => (
|
||||
<>
|
||||
<AuthenticationModal
|
||||
open={activeModal?.type === auth}
|
||||
onClose={setActiveModal}
|
||||
isUpdate={activeModal?.type === auth ? activeModal.isUpdate : undefined}
|
||||
/>
|
||||
<BookModal
|
||||
open={activeModal?.type === book}
|
||||
onClose={
|
||||
activeModal?.type === book ? activeModal.onClose : setActiveModal
|
||||
}
|
||||
book={activeModal?.type === book ? activeModal.book : undefined}
|
||||
/>
|
||||
<CheckoutBookModal
|
||||
open={activeModal?.type === checkout}
|
||||
onClose={
|
||||
activeModal?.type === checkout ? activeModal.onClose : setActiveModal
|
||||
}
|
||||
uuid={activeModal?.type === checkout ? activeModal.uuid : ""}
|
||||
checkoutBy={
|
||||
activeModal?.type === checkout ? activeModal.checkoutBy : undefined
|
||||
}
|
||||
title={activeModal?.type === checkout ? activeModal.title : ""}
|
||||
contact={activeModal?.type === checkout ? activeModal.contact : undefined}
|
||||
/>
|
||||
<ScannerModal
|
||||
open={activeModal?.type === scanner}
|
||||
onClose={setActiveModal}
|
||||
onDetected={
|
||||
activeModal?.type === scanner ? activeModal.onDetect : undefined
|
||||
}
|
||||
/>
|
||||
<DeleteBookModal
|
||||
open={activeModal?.type === del}
|
||||
onClose={activeModal?.type === del ? activeModal.onClose : setActiveModal}
|
||||
uuid={activeModal?.type === checkout ? activeModal.uuid : ""}
|
||||
/>
|
||||
</>
|
||||
);
|
57
frontend/src/shared/components/modals/ScannerModal.tsx
Normal file
57
frontend/src/shared/components/modals/ScannerModal.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { ImCamera } from "react-icons/im";
|
||||
import { primaryRGBA } from "../../../colors";
|
||||
import { useScanner } from "../../utils/useScanner";
|
||||
import { ModalHeader } from "./ModalHeader";
|
||||
|
||||
export const ScannerModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onDetected,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onDetected?: (isbn: string) => void;
|
||||
}): React.JSX.Element => {
|
||||
const { scannerError, setScannerRef } = useScanner({ onDetected });
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={open}
|
||||
onHide={onClose}
|
||||
style={{ backgroundColor: primaryRGBA }}
|
||||
centered
|
||||
>
|
||||
<ModalHeader
|
||||
onClose={onClose}
|
||||
title={"Scan barcode"}
|
||||
icon={<ImCamera size={50} className='ml-0 mr-auto' />}
|
||||
/>
|
||||
<Modal.Body style={{ border: "2px solid black", borderTop: "none" }}>
|
||||
<div
|
||||
className='w-100 overflow-hidden'
|
||||
ref={(ref) => setScannerRef(ref)}
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "25vh",
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
className='drawingBuffer w-100 position-absolute'
|
||||
style={{
|
||||
height: "100%",
|
||||
border: "2px solid black",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{scannerError ? (
|
||||
<p>
|
||||
ERROR INITIALIZING CAMERA ${JSON.stringify(scannerError)} -- DO YOU
|
||||
HAVE PERMISSION?
|
||||
</p>
|
||||
) : null}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
47
frontend/src/shared/components/modals/types.ts
Normal file
47
frontend/src/shared/components/modals/types.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Book } from "../../../types/Book";
|
||||
|
||||
type BaseModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const modalTypes = {
|
||||
auth: "auth",
|
||||
book: "book",
|
||||
checkout: "checkout",
|
||||
scanner: "scanner",
|
||||
del: "del",
|
||||
} as const;
|
||||
|
||||
export type ModalTypes = keyof typeof modalTypes;
|
||||
|
||||
export type AuthModalProps = {
|
||||
type: "auth";
|
||||
isUpdate?: boolean;
|
||||
} & BaseModalProps;
|
||||
|
||||
export type BookModalProps = {
|
||||
type: "book";
|
||||
book?: Book;
|
||||
} & BaseModalProps;
|
||||
|
||||
export type CheckoutBookModalProps = {
|
||||
type: "checkout";
|
||||
} & BaseModalProps &
|
||||
Pick<Book, "uuid" | "title"> &
|
||||
Partial<Pick<Book, "checkoutBy" | "contact">>;
|
||||
|
||||
export type ScannnerModal = {
|
||||
type: "scanner";
|
||||
onDetect: (isbn: string) => void;
|
||||
} & BaseModalProps;
|
||||
|
||||
export type DeleteBookModalProps = { type: "del" } & BaseModalProps &
|
||||
Pick<Book, "uuid">;
|
||||
|
||||
export type ActiveModalProps =
|
||||
| Omit<AuthModalProps, "open" | "onClose">
|
||||
| Omit<BookModalProps, "open">
|
||||
| Omit<CheckoutBookModalProps, "open">
|
||||
| Omit<ScannnerModal, "open" | "onClose">
|
||||
| Omit<DeleteBookModalProps, "open">;
|
Loading…
Add table
Add a link
Reference in a new issue