Skip to content

Commit bb5a2be

Browse files
feat(micro): add pagination to TUI browser
- Implement offset-based pagination with 20 posts per page - Add Previous/Next navigation controls - Show current page and total pages in UI - Automatically adjust page when deleting last post on a page - Refactor showPostActions to return boolean indicating deletion Co-authored-by: Justin Bennett <just-be-dev@users.noreply.github.com>
1 parent b08f54d commit bb5a2be

1 file changed

Lines changed: 76 additions & 23 deletions

File tree

packages/micro/src/browse.ts

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as p from "@clack/prompts";
2-
import { desc, eq } from "drizzle-orm";
2+
import { count, desc, eq } from "drizzle-orm";
33
import { getD1Database, microPosts } from "./db.ts";
44

55
type Post = typeof microPosts.$inferSelect;
66

7+
const PAGE_SIZE = 20;
8+
79
export async function browse() {
810
p.intro("Micro Blog Browser");
911

@@ -13,21 +15,34 @@ export async function browse() {
1315
try {
1416
const { db, dispose } = await getD1Database();
1517

16-
// Fetch all posts, newest first
17-
const posts = await db.select().from(microPosts).orderBy(desc(microPosts.createdAt));
18+
// Get total count
19+
const [{ total }] = await db.select({ total: count() }).from(microPosts);
1820

19-
spinner.stop(`Found ${posts.length} post(s)`);
21+
spinner.stop(`Found ${total} post(s)`);
2022

21-
if (posts.length === 0) {
23+
if (total === 0) {
2224
p.note("No posts yet. Create one with 'micro post'", "Empty");
2325
await dispose();
2426
return;
2527
}
2628

29+
// Track pagination state
30+
let currentPage = 0;
31+
const totalPages = Math.ceil(total / PAGE_SIZE);
32+
2733
// Show posts in a loop until user exits
2834
let shouldContinue = true;
2935

3036
while (shouldContinue) {
37+
// Fetch current page of posts
38+
const offset = currentPage * PAGE_SIZE;
39+
const posts = await db
40+
.select()
41+
.from(microPosts)
42+
.orderBy(desc(microPosts.createdAt))
43+
.limit(PAGE_SIZE)
44+
.offset(offset);
45+
3146
const options = posts.map((post) => {
3247
const preview =
3348
post.content.length > 60 ? post.content.substring(0, 60) + "..." : post.content;
@@ -39,14 +54,34 @@ export async function browse() {
3954
};
4055
});
4156

57+
// Add pagination controls
58+
const hasPrevious = currentPage > 0;
59+
const hasNext = currentPage < totalPages - 1;
60+
61+
if (hasPrevious) {
62+
options.push({
63+
value: -2,
64+
label: "← Previous page",
65+
hint: `Page ${currentPage}/${totalPages}`,
66+
});
67+
}
68+
69+
if (hasNext) {
70+
options.push({
71+
value: -3,
72+
label: "Next page →",
73+
hint: `Page ${currentPage + 2}/${totalPages}`,
74+
});
75+
}
76+
4277
options.push({
4378
value: -1,
4479
label: "Exit",
4580
hint: "",
4681
});
4782

4883
const selectedId = await p.select({
49-
message: "Select a post to view options",
84+
message: `Select a post to view options (Page ${currentPage + 1}/${totalPages})`,
5085
options,
5186
});
5287

@@ -55,11 +90,34 @@ export async function browse() {
5590
continue;
5691
}
5792

93+
// Handle pagination
94+
if (selectedId === -2) {
95+
currentPage = Math.max(0, currentPage - 1);
96+
continue;
97+
}
98+
99+
if (selectedId === -3) {
100+
currentPage = Math.min(totalPages - 1, currentPage + 1);
101+
continue;
102+
}
103+
58104
const selectedPost = posts.find((p) => p.id === selectedId);
59105
if (!selectedPost) continue;
60106

61107
// Show post details and actions
62-
await showPostActions(db, selectedPost, posts);
108+
const deleted = await showPostActions(db, selectedPost);
109+
110+
// If post was deleted, refresh the page
111+
if (deleted) {
112+
// Reload total count
113+
const [{ total: newTotal }] = await db.select({ total: count() }).from(microPosts);
114+
const newTotalPages = Math.ceil(newTotal / PAGE_SIZE);
115+
116+
// If we deleted the last post on a page, go back one page
117+
if (currentPage >= newTotalPages && currentPage > 0) {
118+
currentPage = newTotalPages - 1;
119+
}
120+
}
63121
}
64122

65123
await dispose();
@@ -73,8 +131,7 @@ export async function browse() {
73131
async function showPostActions(
74132
db: ReturnType<typeof getD1Database> extends Promise<{ db: infer D }> ? D : never,
75133
post: Post,
76-
allPosts: Post[],
77-
) {
134+
): Promise<boolean> {
78135
const syndicatedData =
79136
(post.syndicatedTo as Array<{ platform: string; id: string; url: string }> | null) || [];
80137
const syndicatedPlatforms = syndicatedData.map((s) => s.platform);
@@ -105,7 +162,7 @@ async function showPostActions(
105162
});
106163

107164
if (p.isCancel(action) || action === "back") {
108-
return;
165+
return false;
109166
}
110167

111168
switch (action) {
@@ -114,27 +171,27 @@ async function showPostActions(
114171
const url = `https://just-be.dev/micro#post-${post.id}`;
115172
console.log(`Opening: ${url}`);
116173
await Bun.spawn(["open", url]);
117-
break;
174+
return false;
118175
}
119176
case "open-bluesky": {
120177
const blueskyData = syndicatedData.find((s) => s.platform === "bluesky");
121178
if (!blueskyData) {
122179
p.note("This post hasn't been syndicated to Bluesky yet", "Not available");
123-
break;
180+
return false;
124181
}
125182
console.log(`Opening: ${blueskyData.url}`);
126183
await Bun.spawn(["open", blueskyData.url]);
127-
break;
184+
return false;
128185
}
129186
case "open-twitter": {
130187
const twitterData = syndicatedData.find((s) => s.platform === "twitter");
131188
if (!twitterData) {
132189
p.note("This post hasn't been syndicated to Twitter yet", "Not available");
133-
break;
190+
return false;
134191
}
135192
console.log(`Opening: ${twitterData.url}`);
136193
await Bun.spawn(["open", twitterData.url]);
137-
break;
194+
return false;
138195
}
139196
case "delete": {
140197
const confirmed = await p.confirm({
@@ -144,22 +201,18 @@ async function showPostActions(
144201

145202
if (p.isCancel(confirmed) || !confirmed) {
146203
p.note("Delete cancelled", "Cancelled");
147-
break;
204+
return false;
148205
}
149206

150207
const spinner = p.spinner();
151208
spinner.start("Deleting post...");
152209

153210
await db.delete(microPosts).where(eq(microPosts.id, post.id));
154211

155-
// Remove from the posts array
156-
const index = allPosts.findIndex((p) => p.id === post.id);
157-
if (index > -1) {
158-
allPosts.splice(index, 1);
159-
}
160-
161212
spinner.stop(`Post #${post.id} deleted successfully`);
162-
break;
213+
return true;
163214
}
164215
}
216+
217+
return false;
165218
}

0 commit comments

Comments
 (0)