From c866aa920c8337d616f45a7cc77cce2207882eba Mon Sep 17 00:00:00 2001 From: Ohm Patel Date: Tue, 20 Feb 2024 22:06:26 -0500 Subject: [PATCH] add basic Catalogue page --- __tests__/lib/book.test.ts | 188 +++++++++++++++++++++--------------- __tests__/lib/utils.ts | 39 ++++++++ package.json | 2 +- src/app/admin/page.tsx | 2 +- src/app/catalogue/page.tsx | 9 +- src/app/catalogue/table.tsx | 162 +++++++++++++++++++++++++++++++ src/app/globals.css | 5 +- src/app/modify/page.tsx | 2 +- src/app/page.tsx | 2 +- src/lib/api.ts | 46 +++++++++ src/lib/book.ts | 22 +++-- src/lib/utils.ts | 6 ++ src/models/book.ts | 10 +- 13 files changed, 394 insertions(+), 101 deletions(-) create mode 100644 __tests__/lib/utils.ts create mode 100644 src/app/catalogue/table.tsx create mode 100644 src/lib/api.ts diff --git a/__tests__/lib/book.test.ts b/__tests__/lib/book.test.ts index c6a23d4..8242ec8 100644 --- a/__tests__/lib/book.test.ts +++ b/__tests__/lib/book.test.ts @@ -1,85 +1,117 @@ import { DetailedBook } from "@/lib/book"; +import { cloneInstance } from "@/lib/utils"; +import type { Book } from "@/models/book"; -test("Correct book full", () => { - expect( - () => - new DetailedBook({ - id: 1, - title: "The Title", - category: ["fiction", "signed"], - last_updated: new Date(), - deleted: false, - subtitle: "🐄🐮🐄🐮🐄", - authors: "Polish cow", - isbn: "1234567890", - lccn: "1234567890", - edition: "1st", - publisher: "The Publisher", - publish_year: 2021, - publish_month: "January", - publish_location: "Poland", - pages: 100, - weight: "100g", - }) - ).not.toThrow(); +describe("DetailedBook constructor tests", () => { + test("Correct book full", () => { + expect( + () => + new DetailedBook({ + id: 1, + title: "The Title", + category: ["fiction", "signed"], + last_updated: new Date(), + deleted: false, + subtitle: "🐄🐮🐄🐮🐄", + authors: "Polish cow; Tom Scott", + isbn: "1234567890", + lccn: "1234567890", + edition: "1st", + publisher: "The Publisher", + publish_year: 2021, + publish_month: "January", + publish_location: "Poland", + pages: 100, + weight: "100g", + }) + ).not.toThrow(); + }); + + test("Correct book partial", () => { + expect( + () => + new DetailedBook({ + id: 1, + title: "The Title", + category: ["fiction", "signed"], + isbn: "1234567890", + authors: "Polish cow; Tom Scott", + subtitle: "🐄🐮🐄🐮🐄", + }) + ).not.toThrow(); + }); + + test("Incorrect book no title", () => { + expect( + () => + new DetailedBook({ + id: 1, + category: ["fiction", "signed"], + last_updated: new Date("2021-01-01T00:00:00Z"), + deleted: false, + subtitle: "🐄🐮🐄🐮🐄", + authors: "Polish cow", + isbn: "1234567890", + lccn: "1234567890", + edition: "1st", + publisher: "The Publisher", + publish_year: 2021, + publish_month: "January", + publish_location: "Poland", + pages: 100, + weight: "100g", + } as Book) + ).toThrow(); + }); + + test("Incorrect book bad date", () => { + expect( + () => + new DetailedBook({ + id: 1, + title: "The Title", + category: ["fiction", "signed"], + last_updated: "banana", + deleted: false, + subtitle: "🐄🐮🐄🐮🐄", + authors: "Polish cow", + isbn: "1234567890", + lccn: "1234567890", + edition: "1st", + publisher: "The Publisher", + publish_year: 2021, + publish_month: "January", + publish_location: "Poland", + pages: 100, + weight: "100g", + } as unknown as Book) + ).toThrow(); + }); }); -test("Correct book partial", () => { - expect( - () => - new DetailedBook({ - id: 1, - title: "The Title", - category: ["fiction", "signed"], - last_updated: new Date("2021-01-01T00:00:00Z"), - deleted: false, - }) - ).not.toThrow(); -}); +describe("Getters and setters", () => { + const book = new DetailedBook({ + id: 1, + title: "The Title", + category: ["fiction", "signed"], + isbn: "1234567890", + authors: "Polish cow; Tom Scott", + subtitle: "🐄🐮🐄🐮🐄", + }); -test("Incorrect book no title", () => { - expect( - () => - new DetailedBook({ - id: 1, - category: ["fiction", "signed"], - last_updated: new Date("2021-01-01T00:00:00Z"), - deleted: false, - subtitle: "🐄🐮🐄🐮🐄", - authors: "Polish cow", - isbn: "1234567890", - lccn: "1234567890", - edition: "1st", - publisher: "The Publisher", - publish_year: 2021, - publish_month: "January", - publish_location: "Poland", - pages: 100, - weight: "100g", - } as unknown as DetailedBook) - ).toThrow(); -}); + test("authorsList getter", () => { + const clone = cloneInstance(book); + clone.authors = null; + expect(book.authorsList).toEqual(["Polish cow", "Tom Scott"]); + expect(book.authorsList.find((v) => v.includes(";"))).toBeUndefined(); + expect(clone.authorsList).toBeInstanceOf(Array); + expect(clone.authorsList).toHaveLength(0); + }); -test("Incorrect book bad date", () => { - expect( - () => - new DetailedBook({ - id: 1, - title: "The Title", - category: ["fiction", "signed"], - last_updated: "banana", - deleted: false, - subtitle: "🐄🐮🐄🐮🐄", - authors: "Polish cow", - isbn: "1234567890", - lccn: "1234567890", - edition: "1st", - publisher: "The Publisher", - publish_year: 2021, - publish_month: "January", - publish_location: "Poland", - pages: 100, - weight: "100g", - } as unknown as DetailedBook) - ).toThrow(); + test("authorsList setter", () => { + const clone = cloneInstance(book); + clone.authorsList = ["Tom Scott", "Polish cow"]; + + expect(clone.authors).toEqual("Tom Scott; Polish cow"); + }); }); diff --git a/__tests__/lib/utils.ts b/__tests__/lib/utils.ts new file mode 100644 index 0000000..ae181c3 --- /dev/null +++ b/__tests__/lib/utils.ts @@ -0,0 +1,39 @@ +import { cloneInstance } from "@/lib/utils"; + +describe("cloneInstance", () => { + class TestClass { + a: number; + b: number; + c: number; + + constructor(a: number, b: number, c: number) { + this.a = a; + this.b = b; + this.c = c; + } + + equals(other: TestClass) { + return this.a === other.a && this.b === other.b && this.c === other.c; + } + + toString() { + return `TestClass(${this.a}, ${this.b}, ${this.c})`; + } + } + + test("Clones an object", () => { + const obj = { a: 1, b: 2, c: 3 }; + const clone = cloneInstance(obj); + expect(clone).toEqual(obj); + expect(clone).not.toBe(obj); + }); + + test("Clones an instance of a class", () => { + const obj = new TestClass(1, 2, 3); + const clone = cloneInstance(obj); + expect(clone.equals(obj)).toBe(true); + expect(clone).not.toBe(obj); + expect(clone).toBeInstanceOf(TestClass); + expect(clone.toString()).toBe(obj.toString()); + }); +}); diff --git a/package.json b/package.json index 50f638a..1f35eaa 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Ohm Patel ", "license": "GPL-3.0-or-later", "scripts": { - "dev": "next dev --turbo", + "dev": "next dev --turbo -p 3001", "build": "next build", "start": "next start", "lint": "prettier -w ./__tests__ ./src && next lint --fix && eslint ./__tests__ --fix", diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index f5f130e..39e7a8e 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,6 +1,6 @@ export default function Home() { return ( -
+

Admin Page

); diff --git a/src/app/catalogue/page.tsx b/src/app/catalogue/page.tsx index ff654e4..3ed34dc 100644 --- a/src/app/catalogue/page.tsx +++ b/src/app/catalogue/page.tsx @@ -1,7 +1,12 @@ +"use client"; + +import { BookTable } from "./table"; + export default function Home() { return ( -
-

Library page

+
+

Catalogue

+ {BookTable()}
); } diff --git a/src/app/catalogue/table.tsx b/src/app/catalogue/table.tsx new file mode 100644 index 0000000..fa9f8fb --- /dev/null +++ b/src/app/catalogue/table.tsx @@ -0,0 +1,162 @@ +import { getBooks } from "@/lib/api"; +import { DetailedBook } from "@/lib/book"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeadCell, + TableRow, +} from "flowbite-react"; + +import { useState, useEffect, Dispatch, SetStateAction } from "react"; + +export function BookTable() { + const [books, setBooks] = useState([] as DetailedBook[]); + const [nameFilter, setNameFilter] = useState(null as string | null); + const [catFilter, setCatFilter] = useState([] as string[]); + + const [isLoading, setLoading] = useState(true); + + useEffect(() => { + getBooks().then((data) => { + if (!data) { + return setLoading(false); + } + setBooks(data); + setLoading(false); + }); + }, []); + + if (isLoading) return

Loading...

; + if (books.length <= 0) return

No library data

; + + return ( +
+ {renderOptions(setNameFilter, setCatFilter)} +
+ {renderTable(books, nameFilter, catFilter)} +
+
+ ); +} + +let searchNameTo: NodeJS.Timeout; + +function renderOptions( + setNameFilter: Dispatch>, + setCatFilter: Dispatch> +) { + const handleNameChange = (e: React.ChangeEvent) => { + setCatFilter; //TODO: implement category selector + + clearTimeout(searchNameTo); + searchNameTo = setTimeout(() => { + setNameFilter(e.target.value || null); + }, 500); + }; + + return ( +
+
+ +
+
+ +
+ handleNameChange(e)} + required + /> +
+
+
+ ); +} + +function renderTable( + books: DetailedBook[], + nameFilter: string | null, + categoryFilter: string[] +) { + console.log("namef", nameFilter); + console.log("catf", categoryFilter); + + if (nameFilter || categoryFilter.length > 0) { + books = books.filter((book) => { + let pass = true; + + if (categoryFilter.length > 0) { + if (book.category.length <= 0) return false; + for (const category of book.category) { + pass = pass && categoryFilter.includes(category); + } + } + + if (nameFilter) { + const namePresent = + book.title.toLowerCase().includes(nameFilter.toLowerCase()) || + book.authorsList.findIndex((author) => + author.toLowerCase().includes(nameFilter.toLowerCase()) + ) >= 0; + + pass = pass && namePresent; + } + return pass; + }); + } + + let tableRows = books.map((book) => ( + + {book.title} + {book.authors} + {book.category.join(", ")} + {book.isbn} + + )); + + if (tableRows.length <= 0) { + tableRows = [ + + + No results + + , + ]; + } + + return ( + + + Title + Authors + Category + ISBN + + {tableRows} +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 610cc42..3c27aeb 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -21,12 +21,13 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( + /* background: linear-gradient( to bottom, transparent, rgb(var(--background-end-rgb)) ) - rgb(var(--background-start-rgb)); + rgb(var(--background-start-rgb)); */ + background-color: --background-start-rgb; } @layer utilities { diff --git a/src/app/modify/page.tsx b/src/app/modify/page.tsx index d2d4e32..6b19b35 100644 --- a/src/app/modify/page.tsx +++ b/src/app/modify/page.tsx @@ -1,6 +1,6 @@ export default function Home() { return ( -
+

Modify page

); diff --git a/src/app/page.tsx b/src/app/page.tsx index f0e7bc5..389f9e6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ export default function Home() { return ( -
+

Beep Boop

); diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..63f93a1 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,46 @@ +import { DetailedBook } from "./book"; + +export async function getBooks() { + let bookRes = []; + try { + const res = await (await fetch("http://localhost:3000/books")).json(); + if (!res.success) { + throw new Error("Failed to fetch/json parse books"); + } + bookRes = res.result; + } catch (err) { + console.error("Failed to fetch books: " + err); + return null; + } + + const books: DetailedBook[] = []; + for (const book of bookRes) { + // Create a new DetailedBook object and push it to the books array. Use object.assign to copy the properties of the book object to the new DetailedBook object only if the properties are defined + try { + const newBook = new DetailedBook( + Object.assign( + book, + { + id: book.id, + title: book.title, + category: book.category?.length > 0 ? book.category : [], + subtitle: book.subtitle || "", + authors: book.authors || "", + isbn: book.isbn || "", + }, + { + last_updated: book.last_updated + ? new Date(book.last_updated) + : undefined, + } + ) + ); + + books.push(newBook); + } catch (err) { + console.error(`Failed to create entry for ${book?.title}: ${err}`); + continue; + } + } + return books; +} diff --git a/src/lib/book.ts b/src/lib/book.ts index 4b3d489..ba9fd10 100644 --- a/src/lib/book.ts +++ b/src/lib/book.ts @@ -1,15 +1,17 @@ import { Book, BookDetailedSchema } from "@/models/book"; // We do not implement Book or ModifyBook and instead only implement DetailedBook for homogeneity + export class DetailedBook { id: number; title: string; + isbn: string | null; category: string[]; - last_updated: Date; - deleted: boolean; - subtitle?: string; - authors?: string; - isbn?: string; + authors: string | null; + subtitle: string | null; + // Optional fields ↓ + last_updated?: Date; + deleted?: boolean; lccn?: string; edition?: string; publisher?: string; @@ -23,12 +25,12 @@ export class DetailedBook { this.id = book.id; this.title = book.title; this.category = book.category; - this.deleted = book.deleted; - this.last_updated = book.last_updated; - // Optional fields ↓ this.subtitle = book.subtitle; this.authors = book.authors; this.isbn = book.isbn; + // Optional fields ↓ + this.deleted = book.deleted; + this.last_updated = book.last_updated; this.lccn = book.lccn; this.edition = book.edition; this.publisher = book.publisher; @@ -51,11 +53,11 @@ export class DetailedBook { // Getters get authorsList() { - return this.authors?.split(",") || []; + return this.authors?.split(";")?.map((v) => v.trim()) || []; } // Setters set authorsList(authors: string[]) { - this.authors = authors.join(", "); + this.authors = authors.join("; "); } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e69de29..5b4c14b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +export function cloneInstance(instance: T): T { + return Object.assign( + Object.create(Object.getPrototypeOf(instance)), + instance + ); +} diff --git a/src/models/book.ts b/src/models/book.ts index e6aac32..ed2e3e7 100644 --- a/src/models/book.ts +++ b/src/models/book.ts @@ -5,10 +5,10 @@ export type Book = z.infer; export const BookDetailedSchema = z.object({ id: z.number(), title: z.string(), - subtitle: z.string().optional(), - authors: z.string().optional(), + subtitle: z.string(), + authors: z.string(), + isbn: z.string(), category: z.array(z.string()), - isbn: z.string().optional(), lccn: z.string().optional(), edition: z.string().optional(), publisher: z.string().optional(), @@ -17,8 +17,8 @@ export const BookDetailedSchema = z.object({ publish_location: z.string().optional(), pages: z.number().optional(), weight: z.string().optional(), - last_updated: z.date(), - deleted: z.boolean(), + last_updated: z.date().optional(), + deleted: z.boolean().optional(), }); /*