Skip to content

Commit

Permalink
feat: cursor-based pagination for comments using "Load more" button (d…
Browse files Browse the repository at this point in the history
…enoland#438)

Simpler alternative to denoland#429

Towards denoland#414
CC @mbhrznr
  • Loading branch information
iuioiua authored Aug 21, 2023
1 parent 1fca62c commit 3aa15cd
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 53 deletions.
10 changes: 6 additions & 4 deletions fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ import * as $32 from "./routes/submit/_middleware.tsx";
import * as $33 from "./routes/submit/index.tsx";
import * as $34 from "./routes/users/[login].tsx";
import * as $$0 from "./islands/Chart.tsx";
import * as $$1 from "./islands/PageInput.tsx";
import * as $$2 from "./islands/VoteButton.tsx";
import * as $$1 from "./islands/CommentsList.tsx";
import * as $$2 from "./islands/PageInput.tsx";
import * as $$3 from "./islands/VoteButton.tsx";

const manifest = {
routes: {
Expand Down Expand Up @@ -81,8 +82,9 @@ const manifest = {
},
islands: {
"./islands/Chart.tsx": $$0,
"./islands/PageInput.tsx": $$1,
"./islands/VoteButton.tsx": $$2,
"./islands/CommentsList.tsx": $$1,
"./islands/PageInput.tsx": $$2,
"./islands/VoteButton.tsx": $$3,
},
baseUrl: import.meta.url,
};
Expand Down
62 changes: 62 additions & 0 deletions islands/CommentsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
import { Comment } from "@/utils/db.ts";
import UserPostedAt from "@/components/UserPostedAt.tsx";
import { LINK_STYLES } from "@/utils/constants.ts";

async function fetchComments(itemId: string, cursor: string) {
let url = `/api/items/${itemId}/comments`;
if (cursor !== "" && cursor !== undefined) url += "?cursor=" + cursor;
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Request failed: GET ${url}`);
return await resp.json() as { comments: Comment[]; cursor: string };
}

function CommentSummary(props: Comment) {
return (
<div class="py-4">
<UserPostedAt {...props} />
<p>{props.text}</p>
</div>
);
}

export default function CommentsList(props: { itemId: string }) {
const commentsSig = useSignal<Comment[]>([]);
const cursorSig = useSignal("");
const isLoadingSig = useSignal(false);

async function loadMoreComments() {
isLoadingSig.value = true;
try {
const { comments, cursor } = await fetchComments(
props.itemId,
cursorSig.value,
);
commentsSig.value = [...commentsSig.value, ...comments];
cursorSig.value = cursor;
} catch (error) {
console.log(error.message);
} finally {
isLoadingSig.value = false;
}
}

useEffect(() => {
loadMoreComments();
}, []);

return (
<div>
{commentsSig.value.map((comment) => (
<CommentSummary key={comment.id} {...comment} />
))}
{cursorSig.value !== "" && !isLoadingSig.value && (
<button onClick={loadMoreComments} class={LINK_STYLES}>
Load more
</button>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if (Deno.env.get("RESET_DENO_KV") === "1") {
/**
* @todo Remove at v1. This is a quick way to migrate Deno KV, as database changes are likely to occur and require adjustment.
*/
if (Deno.env.get("MIGRATE_DENO_KEY") === "1") {
if (Deno.env.get("MIGRATE_DENO_KV") === "1") {
await migrateKv();
}

Expand Down
43 changes: 4 additions & 39 deletions routes/items/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { Handlers, RouteContext } from "$fresh/server.ts";
import ItemSummary from "@/components/ItemSummary.tsx";
import PageSelector from "@/components/PageSelector.tsx";
import { BUTTON_STYLES, INPUT_STYLES } from "@/utils/constants.ts";
import { calcLastPage, calcPageNum, PAGE_LENGTH } from "@/utils/pagination.ts";
import {
type Comment,
createComment,
createNotification,
getAreVotedBySessionId,
getCommentsByItem,
getItem,
getUserBySession,
newCommentProps,
newNotificationProps,
Notification,
} from "@/utils/db.ts";
import UserPostedAt from "@/components/UserPostedAt.tsx";
import { redirect } from "@/utils/redirect.ts";
import Head from "@/components/Head.tsx";
import { SignedInState } from "@/utils/middleware.ts";
import CommentsList from "@/islands/CommentsList.tsx";

export const handler: Handlers<unknown, SignedInState> = {
async POST(req, ctx) {
Expand Down Expand Up @@ -75,39 +72,19 @@ function CommentInput() {
);
}

function CommentSummary(comment: Comment) {
return (
<div class="py-4">
<UserPostedAt
userLogin={comment.userLogin}
createdAt={comment.createdAt}
/>
<p>{comment.text}</p>
</div>
);
}

export default async function ItemsItemPage(
_req: Request,
ctx: RouteContext<undefined, SignedInState>,
) {
const { id } = ctx.params;
const item = await getItem(id);
const itemId = ctx.params.id;
const item = await getItem(itemId);
if (item === null) return await ctx.renderNotFound();

const pageNum = calcPageNum(ctx.url);
const allComments = await getCommentsByItem(id);
const comments = allComments
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.slice((pageNum - 1) * PAGE_LENGTH, pageNum * PAGE_LENGTH);

const [isVoted] = await getAreVotedBySessionId(
[item],
ctx.state.sessionId,
);

const lastPage = calcLastPage(allComments.length, PAGE_LENGTH);

return (
<>
<Head title={item.title} href={ctx.url.href} />
Expand All @@ -117,19 +94,7 @@ export default async function ItemsItemPage(
isVoted={isVoted}
/>
<CommentInput />
<div>
{comments.map((comment) => (
<CommentSummary
{...comment}
/>
))}
</div>
{lastPage > 1 && (
<PageSelector
currentPage={calcPageNum(ctx.url)}
lastPage={lastPage}
/>
)}
<CommentsList itemId={ctx.params.id} />
</main>
</>
);
Expand Down
6 changes: 1 addition & 5 deletions utils/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function getValues<T>(
) {
const values = [];
const iter = kv.list<T>(selector, options);
for await (const { value } of iter) values.push(value);
for await (const entry of iter) values.push(entry.value);
return values;
}

Expand Down Expand Up @@ -335,10 +335,6 @@ export async function deleteComment(comment: Comment) {
if (!res.ok) throw new Error(`Failed to delete comment: ${comment}`);
}

export async function getCommentsByItem(itemId: string) {
return await getValues<Comment>({ prefix: ["comments_by_item", itemId] });
}

export function listCommentsByItem(
itemId: string,
options?: Deno.KvListOptions,
Expand Down
12 changes: 8 additions & 4 deletions utils/db_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import {
collectValues,
type Comment,
compareScore,
createComment,
Expand All @@ -15,7 +16,6 @@ import {
formatDate,
getAllItems,
getAreVotedBySessionId,
getCommentsByItem,
getDatesSince,
getItem,
getItemsByUser,
Expand All @@ -30,6 +30,7 @@ import {
ifUserHasNotifications,
incrVisitsCountByDay,
type Item,
listCommentsByItem,
newCommentProps,
newItemProps,
newNotificationProps,
Expand Down Expand Up @@ -191,16 +192,19 @@ Deno.test("[db] (create/delete)Comment() + getCommentsByItem()", async () => {
const comment1 = { ...genNewComment(), itemId };
const comment2 = { ...genNewComment(), itemId };

assertEquals(await getCommentsByItem(itemId), []);
assertEquals(await collectValues(listCommentsByItem(itemId)), []);

await createComment(comment1);
await createComment(comment2);
await assertRejects(async () => await createComment(comment2));
assertArrayIncludes(await getCommentsByItem(itemId), [comment1, comment2]);
assertArrayIncludes(await collectValues(listCommentsByItem(itemId)), [
comment1,
comment2,
]);

await deleteComment(comment1);
await deleteComment(comment2);
assertEquals(await getCommentsByItem(itemId), []);
assertEquals(await collectValues(listCommentsByItem(itemId)), []);
});

Deno.test("[db] votes", async () => {
Expand Down

0 comments on commit 3aa15cd

Please sign in to comment.