Template more of catalogue
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Ohm Patel 2024-02-21 03:44:35 -05:00
parent c866aa920c
commit 46d7f5518d
7 changed files with 296 additions and 49 deletions

1
.env.sample Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=http://localhost:3000 # Point to y266shen/librarian-rs/librarian-server

2
.gitignore vendored
View File

@ -168,3 +168,5 @@ dist
.yarn/install-state.gz
.pnp.*
*.bat

View File

@ -0,0 +1,117 @@
import { getBookDetail } from "@/lib/api";
import { DetailedBook } from "@/lib/book";
import { Button, Table } from "flowbite-react";
import { useState, useEffect } from "react";
import type { Dispatch, ReactNode, SetStateAction } from "react";
let setId: Dispatch<SetStateAction<number | null>>;
const idRegex = new RegExp(/\|([0-9]+)\|/);
export function handleOverlay(id: string) {
if (id.length <= 3) {
return;
}
const regexRes = idRegex.exec(id)?.[1];
if (!regexRes) {
return;
}
const idInt = parseInt(regexRes);
if (isNaN(idInt)) {
return;
} else if (!isFinite(idInt)) {
return;
}
setId(idInt);
}
export function BookDetailOverlay() {
const [id, setIdI] = useState(null as number | null);
setId = setIdI;
const [book, setBook] = useState(null as DetailedBook | null);
useEffect(() => {
if (!id) return setBook(null);
getBookDetail(id.toString()).then((data) => {
if (!data) {
console.error("Failed to fetch book details");
setBook(null);
alert("Failed to fetch book details. View console for more info.");
return;
}
setBook(data);
});
}, [id]);
if (!book) {
return "";
}
const rows2 = getRows(book);
const rows1 = rows2.splice(0, 7);
return (
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-zinc-400 bg-opacity-75">
<div className="rounded-md bg-white p-8">
<Button
color="failure"
className="relative right-0 top-0 float-right m-0 p-0"
onClick={() => setId(null)}
>
X
</Button>
<h2 className="text-3xl font-bold">{book.title}</h2>
<p className="text-lg">{`(${book.subtitle})`}</p>
<p className="pb-10 text-lg">{book.authors}</p>
<div className="justify-between overflow-x-auto pb-8">
<table className="m-auto w-10/12">
<tr className="m-5 p-10">
<td>
<Table className="max-w-xl">
<Table.Head>
<Table.HeadCell colSpan={5}></Table.HeadCell>
</Table.Head>
<Table.Body className="divide-y">{rows1}</Table.Body>
</Table>
</td>
<td>
<Table className="max-w-xl">
<Table.Head>
<Table.HeadCell colSpan={5}></Table.HeadCell>
</Table.Head>
<Table.Body className="divide-y">{rows2}</Table.Body>
</Table>
</td>
</tr>
</table>
</div>
<Button gradientDuoTone="purpleToBlue" className="float-right">
Checkout
</Button>
</div>
</div>
);
}
function getRows(book: DetailedBook) {
const rows: ReactNode[] = [];
Object.entries(book).forEach(([key, value]) => {
if (key === "id") return;
if (key == "last_updated") return;
rows.push(
<Table.Row key={key}>
<Table.Cell>{key}</Table.Cell>
<Table.Cell>{value || "Undefined"}</Table.Cell>
<Table.Cell>
<a
href="#"
className="font-medium text-cyan-600 hover:underline dark:text-cyan-500"
>
Edit
</a>
</Table.Cell>
</Table.Row>
);
});
console.log(rows);
return rows;
}

View File

@ -1,11 +1,13 @@
"use client";
import { BookDetailOverlay } from "./overlay";
import { BookTable } from "./table";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center p-24">
<h2 className="pb-14 text-4xl font-bold">Catalogue</h2>
{BookDetailOverlay()}
{BookTable()}
</main>
);

View File

@ -1,6 +1,7 @@
import { getBooks } from "@/lib/api";
import { DetailedBook } from "@/lib/book";
import {
Dropdown,
Table,
TableBody,
TableCell,
@ -10,6 +11,21 @@ import {
} from "flowbite-react";
import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { handleOverlay } from "./overlay";
// Todo fix the attrocities below
const categories: Set<string> = new Set();
const setCategories = (books: DetailedBook[]) => {
for (const book of books) {
for (const category of book.category) {
if (!categories.has(category)) {
categories.add(category);
}
}
}
};
export function BookTable() {
const [books, setBooks] = useState([] as DetailedBook[]);
@ -25,6 +41,7 @@ export function BookTable() {
}
setBooks(data);
setLoading(false);
setCategories(data);
});
}, []);
@ -33,7 +50,7 @@ export function BookTable() {
return (
<div className="w-full">
{renderOptions(setNameFilter, setCatFilter)}
{RenderOptions(setNameFilter, setCatFilter)}
<div className="overflow-x-auto">
{renderTable(books, nameFilter, catFilter)}
</div>
@ -43,56 +60,69 @@ export function BookTable() {
let searchNameTo: NodeJS.Timeout;
function renderOptions(
function RenderOptions(
setNameFilter: Dispatch<SetStateAction<string | null>>,
setCatFilter: Dispatch<SetStateAction<string[]>>
) {
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCatFilter; //TODO: implement category selector
clearTimeout(searchNameTo);
searchNameTo = setTimeout(() => {
setNameFilter(e.target.value || null);
}, 500);
};
const handleCatChange = (category: string) => {
if (category === "None") {
setCatFilter([]);
return;
}
setCatFilter([category]); // Todo support multiple categories (just need front end)
};
return (
<div>
<div className="mx-auto max-w-md">
<label
htmlFor="default-search"
className="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Search
</label>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3">
<svg
className="h-4 w-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<input
type="search"
id="default-search"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-4 ps-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="Search book titles or authors..."
onChange={(e) => handleNameChange(e)}
required
/>
</div>
</div>
<div className="pb-3">
<table className="w-9/12 justify-center">
<tr className="m-auto">
<td className="left-0 ml-0 pl-0">
<div className="mx-auto max-w-md">
<label
htmlFor="default-search"
className="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Search
</label>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3">
<svg
className="h-4 w-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<input
type="search"
id="default-search"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-4 ps-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="Search book titles or authors..."
onChange={(e) => handleNameChange(e)}
required
/>
</div>
</div>
</td>
<td>{CategoriesPicker(handleCatChange)}</td>
</tr>
</table>
</div>
);
}
@ -102,23 +132,25 @@ function renderTable(
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);
}
const targetCategories = book.category.map((cat) =>
cat.toLowerCase().trim()
);
const catPresent = categoryFilter.some((cat) => {
return targetCategories.includes(cat.toLowerCase().trim());
});
pass = pass && catPresent;
}
if (nameFilter) {
const namePresent =
book.title.toLowerCase().includes(nameFilter.toLowerCase()) ||
book.subtitle?.toLowerCase()?.includes(nameFilter.toLowerCase()) ||
book.authorsList.findIndex((author) =>
author.toLowerCase().includes(nameFilter.toLowerCase())
) >= 0;
@ -130,7 +162,14 @@ function renderTable(
}
let tableRows = books.map((book) => (
<TableRow key={book.id} className="hover:bg-zinc-400 hover:text-white">
<TableRow
key={book.id}
title={`${book.title} |${book.id}|`}
className="hover:cursor-pointer hover:bg-zinc-400 hover:text-white"
onClick={(e) => {
handleOverlay(e.currentTarget.title);
}}
>
<TableCell>{book.title}</TableCell>
<TableCell>{book.authors}</TableCell>
<TableCell>{book.category.join(", ")}</TableCell>
@ -149,7 +188,7 @@ function renderTable(
}
return (
<Table>
<Table striped>
<TableHead className="bg-zinc-400">
<TableHeadCell className="w-2/5 ">Title</TableHeadCell>
<TableHeadCell>Authors</TableHeadCell>
@ -160,3 +199,33 @@ function renderTable(
</Table>
);
}
function CategoriesPicker(setCat: (category: string) => void) {
const elements: React.ReactNode[] = [];
categories.forEach((category) => {
elements.push(
<Dropdown.Item
key={category}
onClick={() => {
setCat(category);
}}
>
{category}
</Dropdown.Item>
);
});
return (
<Dropdown label={"Select a category"} dismissOnClick={true}>
<Dropdown.Item
key="None"
onClick={() => {
setCat("None");
}}
>
{"None"}
</Dropdown.Item>
{elements}
</Dropdown>
);
}

View File

@ -7,6 +7,9 @@
--background-start-rgb: 255, 255, 255;
--background-end-rgb: 214, 219, 220;
/* --background-end-rgb: 255, 255, 255; */
scrollbar-width: thin;
scrollbar-gutter: stable;
}
/* This breaks navbar. Website still complies to system theme

View File

@ -1,9 +1,12 @@
import { DetailedBook } from "./book";
import { Book } from "@/models/book";
export async function getBooks() {
let bookRes = [];
try {
const res = await (await fetch("http://localhost:3000/books")).json();
const res = await (
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/books`)
).json();
if (!res.success) {
throw new Error("Failed to fetch/json parse books");
}
@ -44,3 +47,53 @@ export async function getBooks() {
}
return books;
}
export async function getBookDetail(id: string) {
let bookRes: Book;
try {
const res = await (
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/books/${id}`)
).json();
if (!res.success) {
throw new Error("Failed to fetch/json parse book detail");
}
bookRes = res.result;
} catch (err) {
console.error("Failed to fetch book details: " + err);
return null;
}
// Convert nulls to undefined for not required fields
const entries: [string, unknown][] = [];
Object.entries(bookRes).forEach(([key, value]) => {
if (value === null) {
entries.push([key, undefined]);
}
});
try {
const newBook = new DetailedBook(
Object.assign(
Object.fromEntries(entries),
{
id: bookRes.id,
title: bookRes.title,
category: bookRes.category?.length > 0 ? bookRes.category : [],
subtitle: bookRes.subtitle || "",
authors: bookRes.authors || "",
isbn: bookRes.isbn || "",
},
{
last_updated: bookRes.last_updated
? new Date(bookRes.last_updated)
: undefined,
}
)
);
return newBook;
} catch (err) {
console.error(`Failed to get details for ${bookRes?.title}: ${err}`);
return null;
}
}