Skip to content

Fixed pagination :) #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/app/api/books/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,39 +58,51 @@ export const getAllBooksController = async (
limit: number = 0,
withStats: boolean = false,
fromDate?: Date,
endDate?: Date
endDate?: Date,
searchQuery?: string
): Promise<{
books: (BookWithRequests | (BookWithRequests & BookStats))[];
total: number;
totalPages: number;
}> => {
try {
// Calculate the offset (skip) for pagination
console.log("🛠 getAllBooksController params:", {
page,
limit,
fromDate,
endDate,
searchQuery, // ✅ log the search query here
});
const skip = page > 0 && limit > 0 ? (page - 1) * limit : undefined;

// create the date filter
const where: Prisma.BookWhereInput = {};

if (fromDate && endDate) {
const toEndOfDay = new Date(endDate);
toEndOfDay.setHours(23, 59, 59, 999);

where.createdAt = {
gte: fromDate,
lte: toEndOfDay,
};
}
// Fetch paginated books and total count

if (searchQuery && searchQuery.trim() !== "") {
where.title = {
contains: searchQuery.trim(),
mode: "insensitive",
};
}

const [books, total] = await Promise.all([
prisma.book.findMany({
where,
skip,
take: limit > 0 ? limit : undefined,
include: {
requests: true, // Include related requests
requests: true,
},
}),
prisma.book.count({ where }), // Get the total number of books
prisma.book.count({ where }),
]);

const booksWithStats = withStats
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/books/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export async function GET(req: Request) {
// grab the date filter parameters - if none are specified then don't worry about it
const fromDateStr = searchParams.get("fromDate");
const endDateStr = searchParams.get("endDate");
const searchQuery = searchParams.get("search") ?? undefined; // <-- 👈 NEW
console.log("📥 API GET /api/books searchQuery:", searchQuery);

const fromDate = fromDateStr ? new Date(fromDateStr) : undefined;
const endDate = endDateStr ? new Date(endDateStr) : undefined;
Expand All @@ -59,7 +61,8 @@ export async function GET(req: Request) {
limit,
withStats,
effectiveFromDate,
effectiveEndDate
effectiveEndDate,
searchQuery
);
//const books = await getAllBooksController();
return NextResponse.json(books);
Expand Down
47 changes: 34 additions & 13 deletions src/app/dashboard/datapage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,58 @@ export default function DataPage() {
range?.from && range?.to
? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`
: "all time";

const [users, setUsers] = useState<User[]>([]);
const [requests, setRequests] = useState<BookRequest[]>([]);
const [bookStats, setBookStats] = useState<Record<number, BookStats>>({});
const [books, setBooks] = useState<Book[]>([]);
const [requestCount, setRequestCount] = useState(0);

const [currentBookPage, setCurrentBookPage] = useState(1); // For Book Catalog
const [totalBookPages, setTotalBookPages] = useState(0); // Total pages for books
const [currentBookPage, setCurrentBookPage] = useState(1);
const [totalBookPages, setTotalBookPages] = useState(0);

const [currentUserPage, setCurrentUserPage] = useState(1);
const [totalUserPages, setTotalUserPages] = useState(0);

const [currentUserPage, setCurrentUserPage] = useState(1); // For User History
const [totalUserPages, setTotalUserPages] = useState(0); // Total pages for users
const [searchData, setSearchData] = useState(""); // NEW: search input

useEffect(() => {
if (activeTab === "Book Catalog") {
const fetchBooks = async () => {
try {
console.log("📚 Fetching books with params:", {
page: currentBookPage,
search: searchData,
from: range?.from,
to: range?.to,
});
const booksResult = await getAllBooks({
page: currentBookPage,
limit: 10,
withStats: true,
fromDate: range?.from,
endDate: range?.to,
search: searchData,
});

if (booksResult) {
const { books: fetchedBooks, totalPages: fetchedTotalPages } =
booksResult as {
books: (BookWithRequests & BookStats)[];
totalPages: number;
};

const bookStats: Record<number, BookStats> = {};
const stats: Record<number, BookStats> = {};
for (const book of fetchedBooks) {
bookStats[book.id] = {
stats[book.id] = {
totalRequests: book.requests ? book.requests.length : 0,
uniqueUsers: book.uniqueUsers || 0,
};
}

setBooks(fetchedBooks);
setTotalBookPages(fetchedTotalPages);
setBookStats(bookStats);
setBookStats(stats);
}
} catch (err) {
console.error("Failed to fetch books:", err);
Expand All @@ -67,7 +79,13 @@ export default function DataPage() {

fetchBooks();
}
}, [currentBookPage, activeTab, range]); // Refetch books when currentBookPage or activeTab changes
}, [currentBookPage, activeTab, range, searchData]); // include searchData

useEffect(() => {
if (activeTab === "Book Catalog") {
setCurrentBookPage(1); // Reset to first page when search changes
}
}, [searchData, activeTab]);

useEffect(() => {
if (activeTab === "User History") {
Expand All @@ -82,6 +100,7 @@ export default function DataPage() {
if (usersResult) {
const { users: fetchedUsers, totalPages: fetchedTotalPages } =
usersResult;

const allRequests = fetchedUsers.flatMap(
(user) => user.requests || []
);
Expand All @@ -100,17 +119,15 @@ export default function DataPage() {

fetchUsers();
}
}, [range, currentUserPage, activeTab]); // Refetch users when currentUserPage or activeTab changes
}, [range, currentUserPage, activeTab]);

useEffect(() => {
const fetchData = async () => {
try {
// promise.allSettled so they can fail independently.
const [requestCountResult] = await Promise.allSettled([
getRequestCount(range?.from, range?.to),
]);

// calculate the number of requests
if (
requestCountResult.status === "fulfilled" &&
requestCountResult.value !== undefined
Expand All @@ -126,6 +143,7 @@ export default function DataPage() {
console.error("Unexpected error in fetchData:", err);
}
};

fetchData();
}, [range]);

Expand Down Expand Up @@ -165,23 +183,25 @@ export default function DataPage() {
</div>
) : null}
</div>

{/* Tab Content */}
</div>

{activeTab === "Overview" && (
<TableOverview
filterInfo={filterText}
requestCount={requestCount}
range={range}
/>
)}

{activeTab === "Book Catalog" && (
<>
<BookCatalog
books={books}
bookStats={bookStats}
range={range}
setRange={setRange}
searchData={searchData}
setSearchData={setSearchData}
/>
{totalBookPages > 1 && (
<div className="pagination-controls flex justify-center mt-4">
Expand Down Expand Up @@ -212,6 +232,7 @@ export default function DataPage() {
)}
</>
)}

{activeTab === "User History" && (
<>
<UserHistory
Expand Down
42 changes: 21 additions & 21 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
"use client";
import React, { useRef } from "react";
import React, { useRef, useState, useEffect } from "react";
import SearchIcon from "../assets/icons/Search";

interface searchBarProps {
setSearchData: ((searchData: string) => void) | null;
interface SearchBarProps {
setSearchData: (searchData: string) => void;
button: React.ReactNode;
button2?: React.ReactNode;
placeholderText: string;
defaultValue?: string;
}

const SearchBar = (props: searchBarProps) => {
const { setSearchData, button, button2, placeholderText } = props;

const SearchBar = ({
setSearchData,
button,
button2,
placeholderText,
defaultValue = "",
}: SearchBarProps) => {
const searchInputRef = useRef<HTMLInputElement>(null);
const [input, setInput] = useState(defaultValue);

useEffect(() => {
setInput(defaultValue); // keep input in sync with parent
}, [defaultValue]);

const clickBar = () => {
if (searchInputRef.current) {
searchInputRef.current.focus();
}
searchInputRef.current?.focus();
};

// may use this function later, also TODO: turn this into a useCallback
// function handleKeyDown(event: { key: string }) {
// if (event.key === "Enter") {

// }
// }

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;

if (setSearchData) {
setSearchData(value.toLowerCase());
}
const value = e.target.value;
setInput(value);
setSearchData(value.toLowerCase());
};

return (
Expand All @@ -46,6 +45,7 @@ const SearchBar = (props: searchBarProps) => {
className="w-full focus:outline-none text-black placeholder-medium-grey-border text-base"
name="search bar"
onChange={handleInputChange}
value={input}
placeholder={placeholderText}
/>
<button>
Expand Down
52 changes: 27 additions & 25 deletions src/components/common/tables/BookCatalog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useState } from "react";
import React from "react";
import { Book } from "@prisma/client";
import SearchBar from "@/components/SearchBar";
import Link from "next/link";
Expand All @@ -14,16 +14,18 @@ interface BookCatalogProps {
books: Book[];
range?: DateRange;
setRange: (range?: DateRange) => void;
searchData: string;
setSearchData: (search: string) => void;
}
const BookCatalog = (props: BookCatalogProps) => {
const { bookStats, books, range, setRange } = props;

const [searchData, setSearchData] = useState("");

const subsetBooks = structuredClone<Book[]>(books).filter((book) =>
book.title.toLowerCase().includes(searchData)
);

const BookCatalog = ({
books,
bookStats,
range,
setRange,
searchData,
setSearchData,
}: BookCatalogProps) => {
return (
<div className="bg-white">
<SearchBar
Expand All @@ -37,18 +39,19 @@ const BookCatalog = (props: BookCatalogProps) => {
}
placeholderText="Search by book title"
setSearchData={setSearchData}
defaultValue={searchData}
/>
<div className="px-16">
<table className="table-auto bg-white w-full font-family-name:var(--font-geist-sans)]">
<thead>
<tr className="bg-gray-100 h-[50px]">
<th className="w-3/6 text-left text-text-default-secondary px-3">
<th className="w-3/6 text-left text-text-default-secondary px-3">
Book title
</th>
<th className="w-1/6 text-left text-text-default-secondary">
<th className="w-1/6 text-left text-text-default-secondary">
No. of times borrowed
</th>
<th className="w-1/6 text-left text-text-default-secondary">
<th className="w-1/6 text-left text-text-default-secondary">
Unique borrowers
</th>
<th className="w-1/6 text-left text-text-default-secondary">
Expand All @@ -58,25 +61,24 @@ const BookCatalog = (props: BookCatalogProps) => {
</tr>
</thead>
<tbody className="divide-y divide-solid">
{subsetBooks
// .filter(requestFilter)
// .sort(sortByDate)
.map((book, index) => (
{books.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-6 text-gray-500">
No books found.
</td>
</tr>
) : (
books.map((book, index) => (
<tr key={index} className="bg-white h-16">
<td className="underline px-3" style={{ color: "#202D74" }}>
<Link href={`books/${book.id}`}>{book.title}</Link>
</td>
<td>
<p>{bookStats[book.id]?.totalRequests || 0}</p>
</td>

<td>
<p>{bookStats[book.id]?.uniqueUsers || 0}</p>
</td>

<td>{bookStats[book.id]?.totalRequests || 0}</td>
<td>{bookStats[book.id]?.uniqueUsers || 0}</td>
<td>{dateToTimeString(book.createdAt)}</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
Expand Down
Loading