From 6694f47d70ad57f3b4ccd65f5cd75e3e57025baf Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Mon, 7 Jul 2025 21:05:27 -0400 Subject: [PATCH] added pagination to a given authors page --- backend/public/Cargo.lock | 111 +++++++++++++++++---- backend/public/Cargo.toml | 2 +- backend/public/src/datasources/authors.rs | 38 +++++-- backend/public/src/datasources/comments.rs | 9 +- backend/public/src/main.rs | 21 ++-- backend/public/src/routes/authors.rs | 43 ++++++-- backend/public/src/routes/comments.rs | 33 +++--- backend/public/src/routes/posts.rs | 18 ++-- backend/public/src/routes/projects.rs | 5 +- backend/public/src/routes/root.rs | 5 - backend/public/src/utils/mod.rs | 1 + backend/public/src/utils/pagination.rs | 13 +++ frontend/components/PaginationControl.tsx | 89 +++++++++++++++++ frontend/components/PostCard.tsx | 4 +- frontend/lib/truncate.ts | 1 + frontend/routes/authors/[id].tsx | 50 +++++++++- 16 files changed, 350 insertions(+), 93 deletions(-) create mode 100644 backend/public/src/utils/pagination.rs create mode 100644 frontend/components/PaginationControl.tsx diff --git a/backend/public/Cargo.lock b/backend/public/Cargo.lock index 51f31a9..ef632b9 100644 --- a/backend/public/Cargo.lock +++ b/backend/public/Cargo.lock @@ -104,7 +104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.4", "bytes", "futures-util", "http", @@ -113,7 +113,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -123,9 +123,43 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "tokio", - "tower 0.5.1", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core 0.5.2", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -146,7 +180,27 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -875,6 +929,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "itoa" version = "1.0.11" @@ -901,9 +966,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libm" @@ -959,6 +1024,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1251,7 +1322,7 @@ dependencies = [ name = "public" version = "0.1.0" dependencies = [ - "axum", + "axum 0.8.4", "chrono", "dotenvy", "fred", @@ -1920,12 +1991,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.1" @@ -1992,17 +2057,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2010,9 +2077,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -2061,14 +2128,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -2109,7 +2176,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "313fa625fea5790ed56360a30ea980e41229cf482b4835801a67ef1922bf63b9" dependencies = [ - "axum", + "axum 0.7.6", "forwarded-header-value", "governor", "http", diff --git a/backend/public/Cargo.toml b/backend/public/Cargo.toml index b626412..bbbacbf 100644 --- a/backend/public/Cargo.toml +++ b/backend/public/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Wyatt J. Miller , pagination: Pagination, ) -> Result, sqlx::Error> { - let offset: i64 = (pagination.page_number - 1) * pagination.page_size; + let offset: i64 = (pagination.page - 1) * pagination.limit; sqlx::query_as!( Author, "SELECT author_id, first_name, last_name, bio, image FROM authors ORDER BY created_at DESC LIMIT $1 OFFSET $2", - pagination.page_size, + pagination.page, offset, ) .fetch_all(pool) @@ -32,13 +35,32 @@ impl AuthorsDatasource { pub async fn get_authors_posts( pool: &Pool, author_id: i32, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - Post, - "SELECT p.post_id, a.first_name, a.last_name, p.title, p.body, p.created_at, a.author_id FROM posts p LEFT JOIN authors a ON a.author_id = p.author_id WHERE p.deleted_at IS NULL AND p.author_id = $1 ORDER BY created_at DESC", + pagination: Pagination, + ) -> Result<(Vec, i64), sqlx::Error> { + let offset: i64 = (pagination.page - 1) * pagination.limit; + println!( + "Author ID: {}, Page: {}, Size: {}, Offset: {}", + author_id, pagination.page, pagination.limit, offset + ); + + let total_count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM posts p WHERE p.deleted_at IS NULL AND p.author_id = $1", author_id ) + .fetch_one(pool) + .await? + .unwrap_or(0); + + let posts_query = sqlx::query_as!( + Post, + "SELECT p.post_id, a.first_name, a.last_name, p.title, p.body, p.created_at, a.author_id FROM posts p LEFT JOIN authors a ON a.author_id = p.author_id WHERE p.deleted_at IS NULL AND p.author_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", + author_id, + pagination.limit, + offset, + ) .fetch_all(pool) - .await + .await?; + + Ok((posts_query, total_count)) } } diff --git a/backend/public/src/datasources/comments.rs b/backend/public/src/datasources/comments.rs index bd88ea9..f8667d3 100644 --- a/backend/public/src/datasources/comments.rs +++ b/backend/public/src/datasources/comments.rs @@ -1,4 +1,7 @@ -use crate::routes::comments::{Comment, CommentInputPayload, Pagination}; +use crate::{ + routes::comments::{Comment, CommentInputPayload}, + utils::pagination::Pagination, +}; use sqlx::{Pool, Postgres}; pub struct CommentsDatasource; @@ -25,8 +28,8 @@ impl CommentsDatasource { pool: &Pool, pagination: Pagination, ) -> Result, sqlx::Error> { - let offset: i64 = (pagination.page_number - 1) * pagination.page_size; - sqlx::query_as!(Comment, "SELECT comment_id, name, body, created_at FROM comments ORDER BY created_at DESC LIMIT $1 OFFSET $2", pagination.page_size, offset) + let offset: i64 = (pagination.page - 1) * pagination.limit; + sqlx::query_as!(Comment, "SELECT comment_id, name, body, created_at FROM comments ORDER BY created_at DESC LIMIT $1 OFFSET $2", pagination.page, offset) .fetch_all(pool) .await } diff --git a/backend/public/src/main.rs b/backend/public/src/main.rs index 694a68c..d3c4ac9 100644 --- a/backend/public/src/main.rs +++ b/backend/public/src/main.rs @@ -121,20 +121,11 @@ async fn main() { // build our application with some routes let app = Router::new() - .nest("/", routes::root::RootRoute::routes()) - .nest("/posts", routes::posts::PostsRoute::routes(&app_state)) - .nest( - "/comments", - routes::comments::CommentsRoute::routes(&app_state), - ) - .nest( - "/authors", - routes::authors::AuthorsRoute::routes(&app_state), - ) - .nest( - "/projects", - routes::projects::ProjectsRoute::routes(&app_state), - ) + .merge(routes::root::RootRoute::routes()) + .merge(routes::posts::PostsRoute::routes(&app_state)) + .merge(routes::comments::CommentsRoute::routes(&app_state)) + .merge(routes::authors::AuthorsRoute::routes(&app_state)) + .merge(routes::projects::ProjectsRoute::routes(&app_state)) .layer(CorsLayer::permissive()) .layer( TraceLayer::new_for_http() @@ -179,3 +170,5 @@ async fn shutdown_signal() { _ = terminate => {}, } } + + diff --git a/backend/public/src/routes/authors.rs b/backend/public/src/routes/authors.rs index 96dd1c1..57f7225 100644 --- a/backend/public/src/routes/authors.rs +++ b/backend/public/src/routes/authors.rs @@ -1,5 +1,5 @@ use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::get, @@ -8,9 +8,12 @@ use axum::{ use fred::types::Expiration; use serde::{Deserialize, Serialize}; -use crate::{datasources::authors::AuthorsDatasource, state::AppState}; - -use super::comments::Pagination; +use crate::{ + datasources::authors::AuthorsDatasource, + routes::posts::Post, + state::AppState, + utils::pagination::{Pagination, PaginationQuery}, +}; #[derive(Deserialize, Serialize, Clone)] pub struct Author { @@ -26,20 +29,31 @@ pub struct AuthorGetOneParams { pub id: i32, } +#[derive(Deserialize, Serialize)] +pub struct AuthorPostsResponse { + posts: Vec, + total_posts: i64, +} + pub struct AuthorsRoute; impl AuthorsRoute { pub fn routes(app_state: &AppState) -> axum::Router { axum::Router::new() - .route("/", get(AuthorsRoute::get_all)) - .route("/:id", get(AuthorsRoute::get_one)) - .route("/:id/posts", get(AuthorsRoute::get_authors_posts)) + .route("/authors", get(AuthorsRoute::get_all)) + .route("/authors/{id}", get(AuthorsRoute::get_one)) + .route("/authors/{id}/posts", get(AuthorsRoute::get_authors_posts)) .with_state(app_state.clone()) } async fn get_all( State(app_state): State, - Json(pagination): Json, + Query(query): Query, ) -> impl IntoResponse { + let pagination = Pagination { + page: query.page.unwrap_or(1), + limit: query.limit.unwrap_or(12), + }; + let mut state = app_state.lock().await; let cached: Option> = state .cache @@ -104,6 +118,7 @@ impl AuthorsRoute { let state = app_state.clone(); tracing::info!("storing database data in cache"); + tokio::spawn(async move { let mut s = state.lock().await; let _ = s @@ -127,12 +142,20 @@ impl AuthorsRoute { async fn get_authors_posts( State(app_state): State, Path(params): Path, + Query(pagination): Query, ) -> impl IntoResponse { + let pagination = Pagination { + page: pagination.page.unwrap_or(1), + limit: pagination.limit.unwrap_or(12), + }; + let state = app_state.lock().await; - match AuthorsDatasource::get_authors_posts(&state.database, params.id).await { - Ok(p) => Ok(Json(p)), + match AuthorsDatasource::get_authors_posts(&state.database, params.id, pagination).await { + Ok((posts, total_posts)) => Ok(Json(AuthorPostsResponse { posts, total_posts })), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } } + + diff --git a/backend/public/src/routes/comments.rs b/backend/public/src/routes/comments.rs index a8a1ab1..8f8f419 100644 --- a/backend/public/src/routes/comments.rs +++ b/backend/public/src/routes/comments.rs @@ -1,6 +1,13 @@ -use crate::{datasources::comments::CommentsDatasource, state::AppState, utils::datetime::*}; +use crate::{ + datasources::comments::CommentsDatasource, + state::AppState, + utils::{ + datetime::*, + pagination::{Pagination, PaginationQuery}, + }, +}; use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, @@ -21,13 +28,6 @@ pub struct CommentInputPayload { pub struct CommentPathParams { id: i32, } - -#[derive(Deserialize, Serialize)] -pub struct Pagination { - pub page_number: i64, - pub page_size: i64, -} - #[derive(sqlx::FromRow, Deserialize, Serialize, Debug, Clone)] pub struct Comment { pub comment_id: i32, @@ -43,9 +43,9 @@ impl CommentsRoute { pub fn routes(app_state: &AppState) -> axum::Router { // add more comment routes here! axum::Router::new() - .route("/post/:id", get(CommentsRoute::get_post_comments)) - .route("/add", post(CommentsRoute::insert_comment)) - .route("/index", get(CommentsRoute::get_comments_index)) + .route("/comments/post/{id}", get(CommentsRoute::get_post_comments)) + .route("/comments/add", post(CommentsRoute::insert_comment)) + .route("/comments/index", get(CommentsRoute::get_comments_index)) .with_state(app_state.clone()) } @@ -96,8 +96,13 @@ impl CommentsRoute { async fn get_comments_index( State(app_state): State, - Json(pagination): Json, + Query(query): Query, ) -> impl IntoResponse { + let pagination = Pagination { + page: query.page.unwrap_or(1), + limit: query.limit.unwrap_or(12), + }; + let state = app_state.lock().await; match CommentsDatasource::get_index_comments(&state.database, pagination).await { @@ -106,3 +111,5 @@ impl CommentsRoute { } } } + + diff --git a/backend/public/src/routes/posts.rs b/backend/public/src/routes/posts.rs index 30c1e83..feea25b 100644 --- a/backend/public/src/routes/posts.rs +++ b/backend/public/src/routes/posts.rs @@ -57,14 +57,14 @@ impl PostsRoute { pub fn routes(app_state: &AppState) -> Router { // add more post routes here! Router::new() - .route("/all", get(PostsRoute::get_all)) - .route("/:id", get(PostsRoute::get_one)) - .route("/recent", get(PostsRoute::get_recent_posts)) - .route("/popular", get(PostsRoute::get_popular_posts)) - .route("/hot", get(PostsRoute::get_hot_posts)) - .route("/featured", get(PostsRoute::get_featured_posts)) - .route("/rss", get(PostsRoute::get_rss_posts)) - .route("/sitemap", get(PostsRoute::get_sitemap)) + .route("/posts/all", get(PostsRoute::get_all)) + .route("/posts/{id}", get(PostsRoute::get_one)) + .route("/posts/recent", get(PostsRoute::get_recent_posts)) + .route("/posts/popular", get(PostsRoute::get_popular_posts)) + .route("/posts/hot", get(PostsRoute::get_hot_posts)) + .route("/posts/featured", get(PostsRoute::get_featured_posts)) + .route("/posts/rss", get(PostsRoute::get_rss_posts)) + .route("/posts/sitemap", get(PostsRoute::get_sitemap)) .with_state(app_state.clone()) } @@ -399,3 +399,5 @@ impl PostsRoute { } } } + + diff --git a/backend/public/src/routes/projects.rs b/backend/public/src/routes/projects.rs index 74dbcd6..b3db630 100644 --- a/backend/public/src/routes/projects.rs +++ b/backend/public/src/routes/projects.rs @@ -1,5 +1,4 @@ use crate::{datasources::projects::ProjectsDatasource, state::AppState, utils::datetime::*}; -use axum::http::{HeaderMap, HeaderValue}; use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use fred::types::Expiration; use serde::{Deserialize, Serialize}; @@ -21,7 +20,7 @@ pub struct ProjectsRoute; impl ProjectsRoute { pub fn routes(app_state: &AppState) -> Router { Router::new() - .route("/", get(ProjectsRoute::get_all)) + .route("/projects", get(ProjectsRoute::get_all)) .with_state(app_state.clone()) } @@ -67,3 +66,5 @@ impl ProjectsRoute { } } } + + diff --git a/backend/public/src/routes/root.rs b/backend/public/src/routes/root.rs index 21ae726..643e359 100644 --- a/backend/public/src/routes/root.rs +++ b/backend/public/src/routes/root.rs @@ -1,15 +1,10 @@ use axum::{ - extract::State, http::StatusCode, response::{Html, IntoResponse}, routing::get, Router, }; -use crate::{datasources::posts::PostsDatasource, state::AppState}; - -use super::posts::Post; - pub struct RootRoute; impl RootRoute { pub fn routes() -> Router { diff --git a/backend/public/src/utils/mod.rs b/backend/public/src/utils/mod.rs index 3600b8e..5be2b78 100644 --- a/backend/public/src/utils/mod.rs +++ b/backend/public/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod datetime; +pub mod pagination; pub mod rss; pub mod sitemap; diff --git a/backend/public/src/utils/pagination.rs b/backend/public/src/utils/pagination.rs new file mode 100644 index 0000000..1cefaa4 --- /dev/null +++ b/backend/public/src/utils/pagination.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct PaginationQuery { + pub page: Option, + pub limit: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct Pagination { + pub page: i64, + pub limit: i64, +} diff --git a/frontend/components/PaginationControl.tsx b/frontend/components/PaginationControl.tsx new file mode 100644 index 0000000..cbb4299 --- /dev/null +++ b/frontend/components/PaginationControl.tsx @@ -0,0 +1,89 @@ +import * as hi from "jsr:@preact-icons/hi2"; + +export function PaginationControl({ + paginatedData, + currentUrl, + authorId, +}: { + paginatedData: PaginatedPosts; + currentUrl: URL; + authorId: number; +}) { + const buildUrl = (page: number, limit?: number) => { + const params = new URLSearchParams(currentUrl.searchParams); + params.set("page", page.toString()); + if (limit) params.set("limit", limit.toString()); + return `${currentUrl.pathname}?${params.toString()}`; + }; + + if (paginatedData.totalPages <= 1) return null; + + return ( +
+ {/* Pagination info and controls */} +
+
+ {paginatedData.hasPrevPage && ( + +
+ + Previous +
+
+ )} + + {/* Page numbers */} +
+ {Array.from( + { length: Math.min(paginatedData.totalPages, 7) }, + (_, i) => { + let pageNum; + if (paginatedData.totalPages <= 7) { + pageNum = i + 1; + } else { + const start = Math.max(1, paginatedData.currentPage - 3); + const end = Math.min(paginatedData.totalPages, start + 6); + pageNum = start + i; + if (pageNum > end) return null; + } + + const isCurrentPage = pageNum === paginatedData.currentPage; + + return ( + + {pageNum} + + ); + }, + )} +
+ + {paginatedData.hasNextPage && ( + +
+ Next + +
+
+ )} +
+ + {/* Quick jump to page */} +
+
+ ); +} diff --git a/frontend/components/PostCard.tsx b/frontend/components/PostCard.tsx index a38e1ae..c63fbf1 100644 --- a/frontend/components/PostCard.tsx +++ b/frontend/components/PostCard.tsx @@ -4,7 +4,7 @@ import { Post } from "../types/index.ts"; export const PostCard = function PostCard({ post }: { post: Post }) { return ( -
+

{post.title}

@@ -17,7 +17,7 @@ export const PostCard = function PostCard({ post }: { post: Post }) { {" "} at {convertUtc(post.created_at)}

-

{truncateString(post.body, 15)}

+

{truncateString(post.body, 30)}

); diff --git a/frontend/lib/truncate.ts b/frontend/lib/truncate.ts index 98aa086..71432d3 100644 --- a/frontend/lib/truncate.ts +++ b/frontend/lib/truncate.ts @@ -1,3 +1,4 @@ export const truncateString = (str: string, maxLength: number) => { + str = str.replace(/<[^>]*>/g, ""); return str.length > maxLength ? `${str.slice(0, maxLength)}...` : str; }; diff --git a/frontend/routes/authors/[id].tsx b/frontend/routes/authors/[id].tsx index d8b3d41..130d3aa 100644 --- a/frontend/routes/authors/[id].tsx +++ b/frontend/routes/authors/[id].tsx @@ -2,13 +2,20 @@ import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; import AuthorCard from "../../components/AuthorCard.tsx"; import { Post } from "../../types/index.ts"; import { PostCarousel } from "../../components/PostCarousel.tsx"; +import { PaginationControl } from "../../components/PaginationControl.tsx"; export const handler: Handlers = { - async GET(_req: Request, ctx: FreshContext) { + async GET(req: Request, ctx: FreshContext) { try { + const url = new URL(req.url); + const page = parseInt(url.searchParams.get("page") || "1"); + const limit = parseInt(url.searchParams.get("limit") || "12"); + const [authorResponse, authorPostResponse] = await Promise.all([ fetch(`${Deno.env.get("BASE_URI_API")}/authors/${ctx.params.id}`), - fetch(`${Deno.env.get("BASE_URI_API")}/authors/${ctx.params.id}/posts`), + fetch( + `${Deno.env.get("BASE_URI_API")}/authors/${ctx.params.id}/posts?page=${page}&limit=${limit}`, + ), ]); const [authorData, authorPostData] = await Promise.all([ @@ -16,9 +23,37 @@ export const handler: Handlers = { authorPostResponse.json(), ]); + let paginatedData: PaginatedPosts; + + if (authorPostData.posts && authorPostData.total_posts !== undefined) { + const totalPages = Math.ceil(authorPostData.total_posts / limit); + paginatedData = { + posts: authorPostData.posts, + currentPage: page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + totalPosts: authorPostData.total_posts, + }; + } else { + const allPosts = Array.isArray(authorPostData) ? authorPostData : []; + const totalPages = Math.ceil(allPosts.length / limit); + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + + paginatedData = { + posts: allPosts.slice(startIndex, endIndex), + currentPage: page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + totalPosts: allPosts.length, + }; + } + return ctx.render({ authorData, - authorPostData, + authorPostData: paginatedData, }); } catch (error) { return ctx.render({ @@ -30,7 +65,7 @@ export const handler: Handlers = { }, }; -export default function AuthorIdentifier({ data }: PageProps) { +export default function AuthorIdentifier({ data, url }: PageProps) { const { authorData, authorPostData, error } = data; if (error) { @@ -52,7 +87,12 @@ export default function AuthorIdentifier({ data }: PageProps) {
- + +
);