Template more of catalogue
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
c866aa920c
commit
46d7f5518d
|
@ -0,0 +1 @@
|
|||
NEXT_PUBLIC_API_URL=http://localhost:3000 # Point to y266shen/librarian-rs/librarian-server
|
|
@ -168,3 +168,5 @@ dist
|
|||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
|
||||
*.bat
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue