Skip to content

Commit 6feb885

Browse files
committed
feat: add pagination to fix rendering performance issue in large list
1 parent 56c29f7 commit 6feb885

File tree

9 files changed

+95
-94
lines changed

9 files changed

+95
-94
lines changed

frontend/src/lib/api/item.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { api } from './api';
22
import type { Item } from './model';
33

4-
type listOptions = {
4+
export type ListFilter = {
55
count?: number;
66
offset?: number;
77
keyword?: string;
@@ -10,7 +10,11 @@ type listOptions = {
1010
bookmark?: boolean;
1111
};
1212

13-
export async function listItems(options?: listOptions) {
13+
export async function listItems(options?: ListFilter) {
14+
if (options) {
15+
// trip undefinded fields: https://github.com/sindresorhus/ky/issues/293
16+
options = JSON.parse(JSON.stringify(options));
17+
}
1418
return api
1519
.get('items', {
1620
searchParams: options

frontend/src/lib/components/ItemAction.svelte

+11-15
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,9 @@
1212
import Button from './ui/button/button.svelte';
1313
import { updateItem } from '$lib/api/item';
1414
import { toast } from 'svelte-sonner';
15-
import { invalidateAll } from '$app/navigation';
15+
import type { Item } from '$lib/api/model';
1616
17-
export let data: {
18-
id: number;
19-
link: string;
20-
unread: boolean;
21-
bookmark: boolean;
22-
};
17+
export let data: Item;
2318
2419
function getActions(
2520
unread: boolean,
@@ -41,31 +36,32 @@
4136
}
4237
$: actions = getActions(data.unread, data.bookmark);
4338
39+
// TODO: use invalidateAll after refactoring ItemAction's parents with page load
4440
async function handleToggleUnread(e: Event) {
4541
e.preventDefault();
4642
try {
4743
await updateItem(data.id, { unread: !data.unread });
48-
invalidateAll();
44+
data.unread = !data.unread;
4945
} catch (e) {
5046
toast.error((e as Error).message);
5147
}
5248
}
5349
54-
function handleExternalLink(e: Event) {
55-
e.preventDefault();
56-
handleToggleUnread(e);
57-
window.open(data.link, '_target');
58-
}
59-
6050
async function handleToggleBookmark(e: Event) {
6151
e.preventDefault();
6252
try {
6353
await updateItem(data.id, { bookmark: !data.bookmark });
64-
invalidateAll();
54+
data.bookmark = !data.bookmark;
6555
} catch (e) {
6656
toast.error((e as Error).message);
6757
}
6858
}
59+
60+
function handleExternalLink(e: Event) {
61+
e.preventDefault();
62+
handleToggleUnread(e);
63+
window.open(data.link, '_target');
64+
}
6965
</script>
7066

7167
<div>

frontend/src/lib/components/ItemList.svelte

+74-18
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,41 @@
33
import { Button } from './ui/button';
44
import ItemAction from './ItemAction.svelte';
55
import * as Select from '$lib/components/ui/select';
6-
import type { Item } from '$lib/api/model';
6+
import * as Pagination from '$lib/components/ui/pagination';
7+
import type { Feed, Item } from '$lib/api/model';
8+
import { listItems, type ListFilter } from '$lib/api/item';
9+
import { toast } from 'svelte-sonner';
10+
import { allFeeds as fetchAllFeeds } from '$lib/api/feed';
711
8-
export let data: Item[];
9-
$: allFeeds = getFeeds(data);
10-
let selectedFeed = 'all';
11-
$: filteredItems = filterFeed(data, selectedFeed);
12+
export let filter: ListFilter = { offset: 0, count: 10 };
1213
13-
function getFeeds(allItems: Item[]) {
14-
const feeds = new Map<number, { id: number; name: string }>();
15-
allItems.map((v) => feeds.set(v.feed.id, v.feed));
16-
return Array.from(feeds.values());
17-
}
14+
if (filter.offset === undefined) filter.offset = 0;
15+
if (filter.count === undefined) filter.count = 10;
16+
17+
fetchAllFeeds()
18+
.then((v) => {
19+
allFeeds = v;
20+
})
21+
.catch((e) => {
22+
toast.error('Failed to fetch feeds data: ' + e);
23+
});
24+
25+
let data: Item[] = [];
26+
let allFeeds: Feed[] = [];
27+
let currentPage = 1;
28+
let total = 0;
29+
30+
$: filter.offset = (currentPage - 1) * (filter?.count || 10);
31+
$: fetchItems(filter);
1832
19-
function filterFeed(allItems: Item[], feedID: string) {
20-
if (feedID === 'all') return allItems;
21-
return allItems.filter((v) => v.feed.id === parseInt(feedID));
33+
async function fetchItems(filter: ListFilter) {
34+
try {
35+
const resp = await listItems(filter);
36+
data = resp.items;
37+
total = resp.total;
38+
} catch (e) {
39+
toast.error((e as Error).message);
40+
}
2241
}
2342
</script>
2443

@@ -27,7 +46,12 @@
2746
items={allFeeds.map((v) => {
2847
return { value: v.id.toString(), label: v.name };
2948
})}
30-
onSelectedChange={(v) => v && (selectedFeed = v.value)}
49+
onSelectedChange={(v) => {
50+
if (!v) return;
51+
const feedID = parseInt(v.value);
52+
filter.feed_id = feedID > 0 ? feedID : undefined;
53+
filter.offset = 0;
54+
}}
3155
>
3256
<Select.Trigger class="w-[180px]">
3357
<Select.Value placeholder="Filter by Feed" />
@@ -40,8 +64,9 @@
4064
</Select.Content>
4165
</Select.Root>
4266
</div>
67+
4368
<ul class="mt-4">
44-
{#each filteredItems as item}
69+
{#each data as item}
4570
<li class="group rounded-md">
4671
<Button
4772
href={'/items?id=' + item.id}
@@ -60,9 +85,7 @@
6085
</div>
6186

6287
<div class="w-full hidden group-hover:inline-flex justify-end">
63-
<ItemAction
64-
data={{ id: item.id, link: item.link, unread: item.unread, bookmark: item.bookmark }}
65-
/>
88+
<ItemAction bind:data={item} />
6689
</div>
6790
</div>
6891
</Button>
@@ -71,3 +94,36 @@
7194
No data
7295
{/each}
7396
</ul>
97+
98+
{#if total > (filter?.count || 10)}
99+
<Pagination.Root
100+
count={total}
101+
perPage={filter.count}
102+
bind:page={currentPage}
103+
let:pages
104+
let:currentPage
105+
class="mt-8"
106+
>
107+
<Pagination.Content class="flex-wrap">
108+
<Pagination.Item>
109+
<Pagination.PrevButton />
110+
</Pagination.Item>
111+
{#each pages as page (page.key)}
112+
{#if page.type === 'ellipsis'}
113+
<Pagination.Item>
114+
<Pagination.Ellipsis />
115+
</Pagination.Item>
116+
{:else}
117+
<Pagination.Item>
118+
<Pagination.Link {page} isActive={currentPage == page.value}>
119+
{page.value}
120+
</Pagination.Link>
121+
</Pagination.Item>
122+
{/if}
123+
{/each}
124+
<Pagination.Item>
125+
<Pagination.NextButton />
126+
</Pagination.Item>
127+
</Pagination.Content>
128+
</Pagination.Root>
129+
{/if}

frontend/src/routes/+page.svelte

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
<script lang="ts">
2-
import type { PageData } from './$types';
32
import ItemList from '$lib/components/ItemList.svelte';
43
import PageHead from '$lib/components/PageHead.svelte';
5-
6-
export let data: PageData;
74
</script>
85

96
<svelte:head>
107
<title>Unread</title>
118
</svelte:head>
129

1310
<PageHead title="Unread" />
14-
<ItemList data={data.items} />
11+
<ItemList filter={{ unread: true }} />

frontend/src/routes/all/+page.svelte

+1-38
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,11 @@
11
<script lang="ts">
2-
import { Button } from '$lib/components/ui/button';
32
import ItemList from '$lib/components/ItemList.svelte';
43
import PageHead from '$lib/components/PageHead.svelte';
5-
import { Loader2Icon } from 'lucide-svelte';
6-
import type { PageData } from './$types';
7-
import { listItems } from '$lib/api/item';
8-
import { toast } from 'svelte-sonner';
9-
10-
export let data: PageData;
11-
$: items = data.items;
12-
const limit = 10;
13-
let page = 1;
14-
let loading = false;
15-
16-
async function handleLoadMore() {
17-
loading = true;
18-
try {
19-
const resp = await listItems({ offset: page * limit, count: limit });
20-
if (resp.items.length === 0) {
21-
toast.warning('No more items');
22-
} else {
23-
items.push(...resp.items);
24-
items = items;
25-
}
26-
page += 1;
27-
} catch (e) {
28-
toast.error((e as Error).message);
29-
}
30-
loading = false;
31-
}
324
</script>
335

346
<svelte:head>
357
<title>All</title>
368
</svelte:head>
379

3810
<PageHead title="All" />
39-
<ItemList data={items} />
40-
{#if items.length > 0}
41-
<Button variant="secondary" class="w-full mt-6" disabled={loading} on:click={handleLoadMore}>
42-
{#if !loading}
43-
Load More
44-
{:else}
45-
<Loader2Icon class="mr-2 h-4 w-4 animate-spin" />
46-
{/if}
47-
</Button>
48-
{/if}
11+
<ItemList filter={{}} />

frontend/src/routes/all/+page.ts

-6
This file was deleted.
+1-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
<script lang="ts">
2-
import type { PageData } from './$types';
32
import ItemList from '$lib/components/ItemList.svelte';
43
import PageHead from '$lib/components/PageHead.svelte';
5-
6-
export let data: PageData;
74
</script>
85

96
<svelte:head>
107
<title>Bookmark</title>
118
</svelte:head>
129

1310
<PageHead title="Bookmark" />
14-
<ItemList data={data.items} />
11+
<ItemList filter={{ bookmark: true }} />

frontend/src/routes/bookmarks/+page.ts

-6
This file was deleted.

frontend/src/routes/items/+page.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
<p class="text-sm text-muted-foreground">
5858
{data.feed.name} / {moment(data.pub_date).format('lll')}
5959
</p>
60-
<ItemAction data={{ id: data.id, link: data.link, unread: data.unread, bookmark: data.bookmark }} />
60+
<ItemAction bind:data />
6161
<article class="mt-6 prose dark:prose-invert prose-lg max-w-full">
6262
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
6363
{@html data.content}

0 commit comments

Comments
 (0)