move-all-books-from-shelf #15

Merged
0Ry5 merged 5 commits from move-all-books-from-shelf into main 2025-01-17 15:32:17 +01:00
9 changed files with 162 additions and 46 deletions
Showing only changes of commit b25c54915f - Show all commits

View file

@ -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 }}>

View file

@ -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 {

View file

@ -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",
}} }}

View file

@ -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>

View file

@ -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"

View file

@ -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;
}; };

View file

@ -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 (

View file

@ -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";

View file

@ -0,0 +1 @@
export const listShelves = `SELECT DISTINCT shelf FROM bookData;`;