diff --git a/.gitignore b/.gitignore index 3c7e834..a9e5c09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env n39env +.n39env .vscode \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 052c22e..d5518dc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -45,12 +45,17 @@ body { letter-spacing: normal; } + tbody > tr > td { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif !important; + } .table > :not(caption) > * > * { background-color: #f2f2e8 !important; color: #5e6268 !important; } - table { + table > thead { letter-spacing: 0.25em; } @@ -151,6 +156,11 @@ body { } table { + tbody > td > div { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif !important; + } letter-spacing: 0.25em; } diff --git a/frontend/src/pages/Library/Library.tsx b/frontend/src/pages/Library/Library.tsx index b40b22f..0aaaae8 100644 --- a/frontend/src/pages/Library/Library.tsx +++ b/frontend/src/pages/Library/Library.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useContext, useMemo } from "react"; +import React, { useCallback, useContext, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Book } from "../../types/Book"; -import { Pagination, Table } from "react-bootstrap"; +import { Button, Form, Table } from "react-bootstrap"; import { createColumnHelper, flexRender, @@ -15,12 +15,13 @@ import { } from "@tanstack/react-table"; import { Actions } from "./components/Actions"; import { ImArrowDown, ImArrowUp } from "react-icons/im"; -import { AiOutlineLeft, AiOutlineRight } from "react-icons/ai"; import { useLocation } from "react-router-dom"; import { format } from "date-fns"; import { useAuth } from "../../shared/utils/useAuthentication"; import { ModalSelector } from "../../shared/components/modals/Modals"; import { ModalContext } from "../../App"; +import { Pagination } from "./components/Pagination"; +import { modalTypes } from "../../shared/components/modals/types"; export const Library = (): React.JSX.Element => { const location = useLocation(); @@ -31,9 +32,21 @@ export const Library = (): React.JSX.Element => { [location] ); - const auth = useAuth(); + const { authenticated } = useAuth(); const modalContext = useContext(ModalContext); + const [selectedBooks, setSelectedBooks] = useState([]); + + const toggleBookSelection = useCallback( + (uuid: Book["uuid"]) => + setSelectedBooks((prev) => + prev.includes(uuid) + ? prev.filter((current) => current !== uuid) + : [...prev, uuid] + ), + [] + ); + const { data: books, refetch } = useQuery({ queryFn: async () => { if ( @@ -50,10 +63,48 @@ export const Library = (): React.JSX.Element => { queryKey: ["books"], }); + const shelf = params.get("shelf"); + const year = params.get("year"); + + const data = useMemo( + () => + books?.filter( + (b) => + (!shelf || b.shelf?.toLowerCase() === shelf?.toLowerCase()) && + (!year || b.published.toString() === year) + ) ?? [], + [books, year, shelf] + ); + const columnHelper = createColumnHelper(); const columns: ColumnDef[] = useMemo( () => [ + authenticated + ? columnHelper.display({ + id: "selection", + header: () => ( + + setSelectedBooks((prev) => + prev.length || !data ? [] : data.map((b) => b.uuid) + ) + } + /> + ), + cell: (props) => ( + toggleBookSelection(props.row.original.uuid)} + /> + ), + }) + : undefined, columnHelper.accessor((book) => book.isbn, { id: "isbn", header: "ISBN", @@ -108,7 +159,7 @@ export const Library = (): React.JSX.Element => { ), }), - auth.authenticated + authenticated ? columnHelper.accessor( (book) => (!book.contact ? "" : book.contact), { @@ -133,26 +184,21 @@ export const Library = (): React.JSX.Element => { ), }), ].filter((c) => typeof c !== "undefined") as ColumnDef[], - [auth.authenticated, columnHelper, modalContext.setActiveModal, refetch] - ); - - const shelf = params.get("shelf"); - const year = params.get("year"); - - const data = useMemo( - () => - books?.filter( - (b) => - (!shelf || b.shelf?.toLowerCase() === shelf?.toLowerCase()) && - (!year || b.published.toString() === year) - ) ?? [], - [books, year, shelf] + [ + authenticated, + columnHelper, + selectedBooks, + data, + toggleBookSelection, + refetch, + modalContext.setActiveModal, + ] ); const [pagination, setPagination] = React.useState({ @@ -179,19 +225,6 @@ export const Library = (): React.JSX.Element => { [table.getState().pagination.pageIndex] ); - const getPage = useCallback( - (n: number) => ( - table.setPageIndex(n)} - > - {n + 1} - - ), - [table, currentPage] - ); - return (
@@ -249,32 +282,32 @@ export const Library = (): React.JSX.Element => {
{table.getPageCount() ? (
- - table.setPageIndex(currentPage - 1)} + + {authenticated && ( + + )}
) : null} diff --git a/frontend/src/pages/Library/components/Actions.tsx b/frontend/src/pages/Library/components/Actions.tsx index 8e523a3..c870324 100644 --- a/frontend/src/pages/Library/components/Actions.tsx +++ b/frontend/src/pages/Library/components/Actions.tsx @@ -108,9 +108,7 @@ export const Actions = ({ - setActiveModal({ type: del, uuid: book.uuid, onClose }) - } + onClick={() => setActiveModal({ type: del, onClose, ...book })} > diff --git a/frontend/src/pages/Library/components/Pagination.tsx b/frontend/src/pages/Library/components/Pagination.tsx new file mode 100644 index 0000000..ee7b531 --- /dev/null +++ b/frontend/src/pages/Library/components/Pagination.tsx @@ -0,0 +1,61 @@ +import React, { useCallback } from "react"; +import { Pagination as BP } from "react-bootstrap"; +import { AiOutlineLeft, AiOutlineRight } from "react-icons/ai"; + +export const Pagination = ({ + currentPage, + getPageCount, + setPageIndex, +}: { + currentPage: number; + getPageCount: () => number; + setPageIndex: (index: number) => void; +}): React.JSX.Element => { + const getPage = useCallback( + (n: number) => ( + setPageIndex(n)} + > + {n + 1} + + ), + [setPageIndex, currentPage] + ); + + return ( + + setPageIndex(currentPage - 1)} + > + + + {getPage(0)} + {currentPage > 3 && } + {currentPage === 3 && getPage(currentPage - 2)} + {currentPage > 1 && getPage(currentPage - 1)} + {currentPage > 0 && getPage(currentPage)} + {currentPage < getPageCount() - 2 && getPage(currentPage + 1)} + {currentPage < getPageCount() - 3 && getPageCount() > 4 && ( + + )} + {currentPage < getPageCount() - 1 && getPage(getPageCount() - 1)} + setPageIndex(currentPage + 1)} + disabled={currentPage >= getPageCount() - 1} + > + + + + ); +}; diff --git a/frontend/src/shared/components/modals/DeleteBookModal.tsx b/frontend/src/shared/components/modals/DeleteBookModal.tsx index 69713c9..ffbd978 100644 --- a/frontend/src/shared/components/modals/DeleteBookModal.tsx +++ b/frontend/src/shared/components/modals/DeleteBookModal.tsx @@ -6,6 +6,7 @@ import { DeleteBookModalProps } from "./types"; export const DeleteBookModal = ({ uuid, + title, open, onClose, }: Omit): React.JSX.Element => { @@ -26,7 +27,7 @@ export const DeleteBookModal = ({ } /> + Do you really want to delete this book?
+ +
+
+
+ ); +}; diff --git a/frontend/src/shared/components/modals/types.ts b/frontend/src/shared/components/modals/types.ts index b031d91..47d7c83 100644 --- a/frontend/src/shared/components/modals/types.ts +++ b/frontend/src/shared/components/modals/types.ts @@ -11,6 +11,7 @@ export const modalTypes = { checkout: "checkout", scanner: "scanner", del: "del", + moveShelf: "moveShelf", } as const; export type ModalTypes = keyof typeof modalTypes; @@ -37,11 +38,21 @@ export type ScannnerModal = { } & BaseModalProps; export type DeleteBookModalProps = { type: "del" } & BaseModalProps & - Pick; + Pick; + +export type MoveShelfModalProps = { type: "moveShelf" } & BaseModalProps & { + books: Book["uuid"][]; + }; + +export interface MoveShelfAction { + target: Book["shelf"]; + books: Book["uuid"][]; +} export type ActiveModalProps = | Omit | Omit | Omit | Omit - | Omit; + | Omit + | Omit; diff --git a/middleware/index.ts b/middleware/index.ts index d12d1ce..2b3ab01 100644 --- a/middleware/index.ts +++ b/middleware/index.ts @@ -2,7 +2,7 @@ import express, { Request, Response, Application } from "express"; import dotenv from "dotenv"; import { createPool } from "mariadb"; -import { Book } from "./types/Book"; +import { Book, MoveShelfAction } from "./types/Book"; import { checkoutBook, findBook, @@ -19,6 +19,7 @@ import session from "express-session"; import { censorBookData } from "./utils/censorBookData"; import * as fs from "fs"; import { checkForThreads } from "./utils/passesSQLInjectionCheck"; +import { moveShelf } from "./queries/moveShelf"; dotenv.config(); @@ -199,6 +200,31 @@ app.post( } ); +app.post( + "/api/shelf/move", + async ( + req: Request, + res: Response + ) => { + await isAuthenticated(req, res, async () => { + const { target } = req.body; + try { + const containsThread = checkForThreads([target], res); + if (containsThread) { + return; + } + + const conn = await pool.getConnection(); + await conn.query(moveShelf(req.body)); + await conn.end(); + res.status(200).send(`Books moved to shelf ${target}.`); + } catch (error) { + res.status(500).send("Internal Server Error: " + error); + } + }); + } +); + app.post( "/api/books/edit", async ( @@ -230,7 +256,7 @@ app.post( } else { await conn.query(editBook(req.body)); await conn.end(); - res.status(200).send("Book moved to shelf"); + res.status(200).send("Book edited."); } } catch (error) { res.status(500).send("Internal Server Error: " + error); diff --git a/middleware/queries/moveShelf.ts b/middleware/queries/moveShelf.ts new file mode 100644 index 0000000..ce3a25a --- /dev/null +++ b/middleware/queries/moveShelf.ts @@ -0,0 +1,6 @@ +import { MoveShelfAction } from "../types/Book"; + +export const moveShelf = ({ target, books }: MoveShelfAction) => + `UPDATE bookData SET shelf='${target}' WHERE uuid IN ('${books.join( + "', '" + )}');`; diff --git a/middleware/types/Book.ts b/middleware/types/Book.ts index a2fea6f..54609a8 100644 --- a/middleware/types/Book.ts +++ b/middleware/types/Book.ts @@ -9,3 +9,8 @@ export type Book = { contact: string | null; shelf: "AVAILABLE" | "FNORD1" | "FNORD2" | "SHARING" | null; }; + +export interface MoveShelfAction { + target: Book["shelf"]; + books: Book["uuid"][]; +}