move-all-books-from-shelf #15
9 changed files with 162 additions and 46 deletions
|
@ -50,7 +50,7 @@ function App() {
|
||||||
height: "100vh",
|
height: "100vh",
|
||||||
width: "100vw",
|
width: "100vw",
|
||||||
fontFamily: "New Amsterdam",
|
fontFamily: "New Amsterdam",
|
||||||
overflow: "scroll",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
|
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
|
||||||
|
|
|
@ -19,7 +19,7 @@ body {
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #5e6268;
|
background-color: #5e6268;
|
||||||
color: #f2f3f4;
|
color: #f2f3f4;
|
||||||
border: 2px solid #5e6268;
|
border: 1px solid #5e6268;
|
||||||
}
|
}
|
||||||
|
|
||||||
form-control {
|
form-control {
|
||||||
|
@ -91,6 +91,16 @@ body {
|
||||||
border: 2px solid #f2f2e8;
|
border: 2px solid #f2f2e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
background-color: #f2f2e8 !important;
|
||||||
|
color: #5e6268 !important;
|
||||||
|
border: 2px solid #f2f2e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: #5e6268 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
background-color: #f9a9ab !important;
|
background-color: #f9a9ab !important;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +133,7 @@ body {
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #f2f3f4;
|
background-color: #f2f3f4;
|
||||||
color: #5e6268;
|
color: #5e6268;
|
||||||
border: 2px solid #5e6268;
|
border-left: 1px solid #5e6268;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
|
@ -179,6 +189,16 @@ body {
|
||||||
border: 2px solid #5e6268;
|
border: 2px solid #5e6268;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
background-color: #f2f2e8 !important;
|
||||||
|
color: #5e6268 !important;
|
||||||
|
border: 2px solid #f2f2e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: #f2f2e8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
color: #f2f2e8 !important;
|
color: #f2f2e8 !important;
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -232,7 +232,7 @@ export const Library = (): React.JSX.Element => {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
maxHeight: "70vh",
|
maxHeight: "80vh",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
marginBottom: "5px",
|
marginBottom: "5px",
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { Button, Col, Form, Modal, Row } from "react-bootstrap";
|
import { Button, Col, Form, Modal, Row } from "react-bootstrap";
|
||||||
import { ImBook, ImCamera } from "react-icons/im";
|
import { ImBook, ImCamera, ImCancelCircle } from "react-icons/im";
|
||||||
import { ModalHeader } from "./ModalHeader";
|
import { ModalHeader } from "./ModalHeader";
|
||||||
import { useForm, useWatch } from "react-hook-form";
|
import { useForm, useWatch } from "react-hook-form";
|
||||||
import { Book, bookShelfs } from "../../../types/Book";
|
import { Book } from "../../../types/Book";
|
||||||
import { useScanner } from "../../utils/useScanner";
|
import { useScanner } from "../../utils/useScanner";
|
||||||
import { BookModalProps } from "./types";
|
import { BookModalProps } from "./types";
|
||||||
import { tryGoogleBooksApi } from "../../../pages/Main/utils/tryGoogleBooksApi";
|
import { tryGoogleBooksApi } from "../../../pages/Main/utils/tryGoogleBooksApi";
|
||||||
import { tryDeutscheNationalBibliothekApi } from "../../../pages/Main/utils/tryDeutscheNationalBibliothekApi";
|
import { tryDeutscheNationalBibliothekApi } from "../../../pages/Main/utils/tryDeutscheNationalBibliothekApi";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
type BookForm = Pick<Book, "isbn" | "title" | "shelf" | "published">;
|
type BookForm = Pick<Book, "isbn" | "title" | "shelf" | "published">;
|
||||||
|
|
||||||
|
@ -19,6 +20,19 @@ export const BookModal = ({
|
||||||
const isEdit = !!book;
|
const isEdit = !!book;
|
||||||
const [showScanner, setShowScanner] = useState(false);
|
const [showScanner, setShowScanner] = useState(false);
|
||||||
|
|
||||||
|
const [addNew, setAddNew] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { data: shelves, isLoading } = useQuery<string[]>({
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/shelves/list`);
|
||||||
|
const data = await response.text();
|
||||||
|
return data
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.replaceAll('"', "").replace("[", "").replace("]", ""));
|
||||||
|
},
|
||||||
|
queryKey: ["shleves"],
|
||||||
|
});
|
||||||
|
|
||||||
const { control, register, formState, setError, setValue, reset } =
|
const { control, register, formState, setError, setValue, reset } =
|
||||||
useForm<BookForm>({
|
useForm<BookForm>({
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
|
@ -84,7 +98,7 @@ export const BookModal = ({
|
||||||
}
|
}
|
||||||
if (error) return;
|
if (error) return;
|
||||||
const createdBook = await fetch(
|
const createdBook = await fetch(
|
||||||
isEdit ? "api/books/edit" : "/api/books/create",
|
isEdit ? "/api/books/edit" : "/api/books/create",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
@ -213,22 +227,52 @@ export const BookModal = ({
|
||||||
<Form.Label>Shelf</Form.Label>
|
<Form.Label>Shelf</Form.Label>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Form.Select
|
{!isLoading &&
|
||||||
{...register("shelf", { required: true })}
|
(addNew ? (
|
||||||
isInvalid={!!formState.errors.shelf}
|
<div className='d-flex'>
|
||||||
>
|
<Form.Control
|
||||||
{Object.keys(bookShelfs).map((key) => (
|
id='move-shelf-text-input'
|
||||||
<option
|
{...register("shelf", { required: true })}
|
||||||
key='key'
|
style={{
|
||||||
value={bookShelfs[key as keyof typeof bookShelfs]}
|
borderRadius: "5px 0px 0px 5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
borderRadius: "0px 5px 5px 0px",
|
||||||
|
}}
|
||||||
|
className='d-flex mr-2 pt-2'
|
||||||
|
onClick={() => setAddNew(false)}
|
||||||
|
>
|
||||||
|
<div className='m-auto'>
|
||||||
|
<ImCancelCircle size={20} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Form.Select
|
||||||
|
id='move-shelf-select'
|
||||||
|
{...register("shelf", {
|
||||||
|
required: true,
|
||||||
|
onChange: (ev) => {
|
||||||
|
if (ev.target.value === "addNew") {
|
||||||
|
setAddNew(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
isInvalid={!!formState.errors.shelf}
|
||||||
>
|
>
|
||||||
{key}
|
{shelves?.map((shelf) => (
|
||||||
</option>
|
<option key='key' value={shelf}>
|
||||||
|
{shelf}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value='addNew'>Add new</option>
|
||||||
|
</Form.Select>
|
||||||
))}
|
))}
|
||||||
</Form.Select>
|
|
||||||
<Form.Control.Feedback type='invalid'>
|
<Form.Control.Feedback type='invalid'>
|
||||||
{!values.shelf
|
{!values.shelf
|
||||||
? "Shelf is required"
|
? "Target shelf is required"
|
||||||
: formState.errors.shelf?.message}
|
: formState.errors.shelf?.message}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Button, Col, Form, Modal, Row } from "react-bootstrap";
|
import { Button, Col, Form, Modal, Row } from "react-bootstrap";
|
||||||
import { ImBook } from "react-icons/im";
|
import { ImBook, ImCancelCircle } from "react-icons/im";
|
||||||
import { ModalHeader } from "./ModalHeader";
|
import { ModalHeader } from "./ModalHeader";
|
||||||
import { MoveShelfAction, MoveShelfModalProps } from "./types";
|
import { MoveShelfAction, MoveShelfModalProps } from "./types";
|
||||||
import { useForm, useWatch } from "react-hook-form";
|
import { useForm, useWatch } from "react-hook-form";
|
||||||
import { bookShelfs } from "../../../types/Book";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const MoveShelfModal = ({
|
export const MoveShelfModal = ({
|
||||||
books,
|
books,
|
||||||
|
@ -20,7 +20,7 @@ export const MoveShelfModal = ({
|
||||||
const { mutate: mv } = useMutation({
|
const { mutate: mv } = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (!values.target) {
|
if (!values.target) {
|
||||||
setError("target", { message: "Taget shelf is required" });
|
setError("target", { message: "Target shelf is required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,19 @@ export const MoveShelfModal = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: shelves, isLoading } = useQuery<string[]>({
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/shelves/list`);
|
||||||
|
const data = await response.text();
|
||||||
|
return data
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.replaceAll('"', "").replace("[", "").replace("]", ""));
|
||||||
|
},
|
||||||
|
queryKey: ["shleves"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [addNew, setAddNew] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={open} onHide={onClose} centered>
|
<Modal show={open} onHide={onClose} centered>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
|
@ -48,23 +61,50 @@ export const MoveShelfModal = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Group as={Row} className='mb-2'>
|
<Form.Group as={Row} className='mb-2'>
|
||||||
<Col sm='2'>
|
<Col className='d-flex' sm='2'>
|
||||||
<Form.Label>Shelf</Form.Label>
|
<Form.Label className='m-auto'>Shelf</Form.Label>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Form.Select
|
{!isLoading &&
|
||||||
{...register("target", { required: true })}
|
(addNew ? (
|
||||||
isInvalid={!!formState.errors.target}
|
<div className='d-flex'>
|
||||||
>
|
<Form.Control
|
||||||
{Object.keys(bookShelfs).map((key) => (
|
id='move-shelf-text-input'
|
||||||
<option
|
{...register("target", { required: true })}
|
||||||
key='key'
|
/>
|
||||||
value={bookShelfs[key as keyof typeof bookShelfs]}
|
<Button
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
marginLeft: "0px",
|
||||||
|
marginRight: "10px",
|
||||||
|
}}
|
||||||
|
className='d-flex m-auto'
|
||||||
|
onClick={() => setAddNew(false)}
|
||||||
|
>
|
||||||
|
<ImCancelCircle className='m-auto primary' size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Form.Select
|
||||||
|
id='move-shelf-select'
|
||||||
|
{...register("target", {
|
||||||
|
required: true,
|
||||||
|
onChange: (ev) => {
|
||||||
|
if (ev.target.value === "addNew") {
|
||||||
|
setAddNew(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
isInvalid={!!formState.errors.target}
|
||||||
>
|
>
|
||||||
{key}
|
{shelves?.map((shelf) => (
|
||||||
</option>
|
<option key='key' value={shelf}>
|
||||||
|
{shelf}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value='addNew'>Add new</option>
|
||||||
|
</Form.Select>
|
||||||
))}
|
))}
|
||||||
</Form.Select>
|
|
||||||
<Form.Control.Feedback type='invalid'>
|
<Form.Control.Feedback type='invalid'>
|
||||||
{!values.target
|
{!values.target
|
||||||
? "Target shelf is required"
|
? "Target shelf is required"
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
export const bookShelfs = {
|
|
||||||
available: "AVAILABLE",
|
|
||||||
fnord1: "FNORD1",
|
|
||||||
fnord2: "FNORD2",
|
|
||||||
sharing: "SHARING",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Shelf = (typeof bookShelfs)[keyof typeof bookShelfs];
|
|
||||||
|
|
||||||
export type Book = {
|
export type Book = {
|
||||||
id: number;
|
id: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
@ -16,5 +7,5 @@ export type Book = {
|
||||||
lastCheckoutDate: number | null;
|
lastCheckoutDate: number | null;
|
||||||
checkoutBy: string | null;
|
checkoutBy: string | null;
|
||||||
contact: string | null;
|
contact: string | null;
|
||||||
shelf: Shelf | null;
|
shelf: string | null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
deleteBook,
|
deleteBook,
|
||||||
editBook,
|
editBook,
|
||||||
returnBook,
|
returnBook,
|
||||||
|
listShelves,
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
@ -370,6 +371,24 @@ app.get(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
"/api/shelves/list",
|
||||||
|
async (
|
||||||
|
req: Request<undefined, undefined, null>,
|
||||||
|
res: Response<string[] | string>
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
const books = await conn.query<Book[]>(listShelves);
|
||||||
|
const shelves = books.map((e) => (!e.shelf ? "AVAILABLE" : e.shelf));
|
||||||
|
await conn.end();
|
||||||
|
res.status(200).send(shelves);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send("Internal Server Error: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/api/books/return",
|
"/api/books/return",
|
||||||
async (
|
async (
|
||||||
|
|
|
@ -6,3 +6,4 @@ export { findBook } from "./findBook";
|
||||||
export { createBook } from "./createBook";
|
export { createBook } from "./createBook";
|
||||||
export { deleteBook } from "./deleteBook";
|
export { deleteBook } from "./deleteBook";
|
||||||
export { editBook } from "./editBook";
|
export { editBook } from "./editBook";
|
||||||
|
export { listShelves } from "./listShelves";
|
||||||
|
|
1
middleware/queries/listShelves.ts
Normal file
1
middleware/queries/listShelves.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const listShelves = `SELECT DISTINCT shelf FROM bookData;`;
|
Loading…
Reference in a new issue