Skip to content

Commit 7d33af7

Browse files
committed
feat: display items by group
1 parent 103ece6 commit 7d33af7

File tree

9 files changed

+174
-82
lines changed

9 files changed

+174
-82
lines changed

frontend/src/lib/api/item.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type ListFilter = {
88
page_size?: number;
99
keyword?: string;
1010
feed_id?: number;
11+
group_id?: number;
1112
unread?: boolean;
1213
bookmark?: boolean;
1314
};
@@ -21,7 +22,7 @@ export async function listItems(options?: ListFilter) {
2122
.get('items', {
2223
searchParams: options
2324
})
24-
.json<{ total: number; items: Omit<Item, 'content'>[] }>();
25+
.json<{ total: number; items: Item[] }>();
2526
}
2627

2728
export function parseURLtoFilter(params: URLSearchParams, override?: ListFilter): ListFilter {

frontend/src/lib/components/ItemList.svelte

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { getFavicon } from '$lib/api/favicon';
55
import { applyFilterToURL, parseURLtoFilter } from '$lib/api/item';
66
import type { Item } from '$lib/api/model';
7+
import { defaultPageSize } from '$lib/consts';
78
import { t } from '$lib/i18n';
89
import ItemActionBookmark from './ItemActionBookmark.svelte';
910
import ItemActionUnread from './ItemActionUnread.svelte';
@@ -162,26 +163,28 @@
162163
{/each}
163164
</ul>
164165

165-
<div class="mt-6 flex w-full flex-wrap justify-center gap-4">
166-
<Pagination
167-
currentPage={filter.page}
168-
pageSize={filter.page_size}
169-
{total}
170-
onPageChange={handleChangePage}
171-
/>
172-
<div class="join">
173-
<input
174-
type="number"
175-
bind:value={filter.page_size}
176-
onchange={handleChangePageSize}
177-
min="10"
178-
step="10"
179-
class="input join-item w-16"
166+
{#if total / (filter.page_size ?? defaultPageSize) > 1}
167+
<div class="mt-6 flex w-full flex-wrap justify-center gap-4">
168+
<Pagination
169+
currentPage={filter.page}
170+
pageSize={filter.page_size}
171+
{total}
172+
onPageChange={handleChangePage}
180173
/>
181-
<span class="join-item bg-base-300 text-base-content/60 flex items-center px-2 text-sm">
182-
Per page
183-
</span>
174+
<div class="join">
175+
<input
176+
type="number"
177+
bind:value={filter.page_size}
178+
onchange={handleChangePageSize}
179+
min="10"
180+
step="10"
181+
class="input join-item w-16"
182+
/>
183+
<span class="join-item bg-base-300 text-base-content/60 flex items-center px-2 text-sm">
184+
Per page
185+
</span>
186+
</div>
184187
</div>
185-
</div>
188+
{/if}
186189
{/if}
187190
</div>

frontend/src/lib/components/Pagination.svelte

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,27 @@
4343
}
4444
</script>
4545

46-
{#if pages.length > 1}
47-
<div class="join">
48-
<button
49-
class="join-item btn"
50-
disabled={currentPage === 1}
51-
onclick={() => handlePageChange(currentPage - 1)}>«</button
52-
>
53-
{#each pages as page}
54-
{#if typeof page === 'string'}
55-
<button class="join-item btn" disabled>...</button>
56-
{:else}
57-
<button
58-
class={`join-item btn ${page === currentPage ? 'btn-active border-b-base-content/60 border-b-2' : ''}`}
59-
onclick={() => handlePageChange(page)}
60-
>
61-
{page}
62-
</button>
63-
{/if}
64-
{/each}
65-
<button
66-
class="join-item btn"
67-
disabled={currentPage === totalPages}
68-
onclick={() => handlePageChange(currentPage + 1)}>»</button
69-
>
70-
</div>
71-
{/if}
46+
<div class="join">
47+
<button
48+
class="join-item btn"
49+
disabled={currentPage === 1}
50+
onclick={() => handlePageChange(currentPage - 1)}>«</button
51+
>
52+
{#each pages as page}
53+
{#if typeof page === 'string'}
54+
<button class="join-item btn" disabled>...</button>
55+
{:else}
56+
<button
57+
class={`join-item btn ${page === currentPage ? 'btn-active border-b-base-content/60 border-b-2' : ''}`}
58+
onclick={() => handlePageChange(page)}
59+
>
60+
{page}
61+
</button>
62+
{/if}
63+
{/each}
64+
<button
65+
class="join-item btn"
66+
disabled={currentPage === totalPages}
67+
onclick={() => handlePageChange(currentPage + 1)}>»</button
68+
>
69+
</div>

frontend/src/lib/components/Sidebar.svelte

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { t } from '$lib/i18n';
88
import {
99
BookmarkCheck,
10+
ChevronDown,
11+
ChevronRight,
1012
CircleEllipsis,
1113
CirclePlus,
1214
Command,
@@ -33,6 +35,9 @@
3335
3436
let { feeds, groups }: Props = $props();
3537
38+
// State to track open groups
39+
let openGroups = $state<Record<number, boolean>>({});
40+
3641
let feedList = $derived.by(async () => {
3742
const [feedsData, groupsData] = await Promise.all([feeds, groups]);
3843
const groupFeeds: { id: number; name: string; feeds: (Feed & { indexInList: number })[] }[] =
@@ -50,6 +55,7 @@
5055
indexInList: curIndexInList++
5156
}))
5257
});
58+
openGroups[group.id] = false;
5359
});
5460
return groupFeeds;
5561
});
@@ -133,7 +139,7 @@
133139
134140
const el = document.getElementById(`sidebar-feed-${selectedFeedIndex}`);
135141
if (el) {
136-
selectedFeedGroupId = parseInt(el.getAttribute('data-group-id') ?? '-1');
142+
openGroups[parseInt(el.getAttribute('data-group-id') ?? '-1')] = true;
137143
el.focus();
138144
// focus twice because <details> element's opening delay blocks the focus when
139145
// we open a new group (<details>)
@@ -193,44 +199,58 @@
193199
</ul>
194200

195201
<ul class="menu w-full">
196-
<li class="menu-title">{t('common.feeds')}</li>
202+
<li class="menu-title text-xs">{t('common.feeds')}</li>
197203
{#await feedList}
198204
<div class="skeleton bg-base-300 h-10"></div>
199205
{:then groupData}
200-
{#each groupData as group, groupIndex}
206+
{#each groupData as group (group.id)}
207+
{@const isOpen = openGroups[group.id]}
201208
<li>
202-
<details open={groupIndex === 0 || selectedFeedGroupId === group.id}>
203-
<summary class="overflow-hidden">
204-
<span class="line-clamp-1">{group.name}</span>
205-
</summary>
206-
<ul>
207-
{#each group.feeds as feed}
208-
{@const textColor = feed.suspended
209-
? 'text-neutral-content/60'
210-
: feed.failure
211-
? 'text-error'
212-
: ''}
213-
<li>
214-
<a
215-
id="sidebar-feed-{feed.indexInList}"
216-
data-group-id={group.id}
217-
href="/feeds/{feed.id}"
218-
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
219-
>
220-
<div class="avatar">
221-
<div class="size-4 rounded-full">
222-
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
223-
</div>
209+
<div class="relative flex items-center pl-10">
210+
<button
211+
class="btn btn-ghost btn-sm btn-square absolute top-0 left-1"
212+
onclick={(event) => {
213+
event.preventDefault();
214+
openGroups[group.id] = !isOpen;
215+
}}
216+
>
217+
{#if isOpen}
218+
<ChevronDown class="size-4" />
219+
{:else}
220+
<ChevronRight class="size-4" />
221+
{/if}
222+
</button>
223+
<a href="/groups/{group.id}" class="line-clamp-1 grow text-left">
224+
{group.name}
225+
</a>
226+
</div>
227+
<ul class:hidden={!isOpen}>
228+
{#each group.feeds as feed}
229+
{@const textColor = feed.suspended
230+
? 'text-neutral-content/60'
231+
: feed.failure
232+
? 'text-error'
233+
: ''}
234+
<li>
235+
<a
236+
id="sidebar-feed-{feed.indexInList}"
237+
data-group-id={group.id}
238+
href="/feeds/{feed.id}"
239+
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
240+
>
241+
<div class="avatar">
242+
<div class="size-4 rounded-full">
243+
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
224244
</div>
225-
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
226-
{#if feed.unread_count > 0}
227-
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
228-
{/if}
229-
</a>
230-
</li>
231-
{/each}
232-
</ul>
233-
</details>
245+
</div>
246+
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
247+
{#if feed.unread_count > 0}
248+
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
249+
{/if}
250+
</a>
251+
</li>
252+
{/each}
253+
</ul>
234254
</li>
235255
{/each}
236256
{/await}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script lang="ts">
2+
import ItemActionMarkAllasRead from '$lib/components/ItemActionMarkAllasRead.svelte';
3+
import ItemList from '$lib/components/ItemList.svelte';
4+
import PageNavHeader from '$lib/components/PageNavHeader.svelte';
5+
import { t } from '$lib/i18n';
6+
import { Settings2 } from 'lucide-svelte';
7+
8+
let { data } = $props();
9+
</script>
10+
11+
<svelte:head>
12+
{#await data.group then group}
13+
<title>{group.name}</title>
14+
{/await}
15+
</svelte:head>
16+
17+
{#await data.group}
18+
Loading...
19+
{:then group}
20+
<PageNavHeader showSearch={true}>
21+
{#await data.items then items}
22+
<ItemActionMarkAllasRead items={items.items} />
23+
{/await}
24+
<div class="tooltip tooltip-bottom" data-tip={t('common.settings')}>
25+
<a href="/settings#groups" class="btn btn-ghost btn-square">
26+
<Settings2 class="size-4" />
27+
</a>
28+
</div>
29+
</PageNavHeader>
30+
31+
<div class="px-4 lg:px-8">
32+
<div class="items-center py-6">
33+
<h1 class="text-3xl font-bold">{group.name}</h1>
34+
</div>
35+
<ItemList data={data.items} highlightUnread={true} />
36+
</div>
37+
{/await}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { allGroups } from '$lib/api/group';
2+
import { listItems, parseURLtoFilter } from '$lib/api/item';
3+
import { error } from '@sveltejs/kit';
4+
import type { PageLoad } from './$types';
5+
6+
export const prerender = false;
7+
8+
export const load: PageLoad = async ({ depends, url, params }) => {
9+
depends(`page:${url.pathname}`);
10+
11+
const id = parseInt(params.id);
12+
const group = allGroups().then((groups) => {
13+
const group = groups.find((g) => g.id === id);
14+
if (!group) {
15+
error(404, 'Group not found');
16+
}
17+
return group;
18+
});
19+
const filter = parseURLtoFilter(url.searchParams, {
20+
unread: undefined,
21+
bookmark: undefined,
22+
feed_id: undefined,
23+
group_id: id
24+
});
25+
const items = listItems(filter);
26+
return { group, items: items };
27+
};

repo/item.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,25 @@ type Item struct {
2222
type ItemFilter struct {
2323
Keyword *string
2424
FeedID *uint
25+
GroupID *uint
2526
Unread *bool
2627
Bookmark *bool
2728
}
2829

2930
func (i Item) List(filter ItemFilter, page, pageSize int) ([]*model.Item, int, error) {
3031
var total int64
3132
var res []*model.Item
32-
db := i.db.Model(&model.Item{})
33+
db := i.db.Model(&model.Item{}).Joins("JOIN feeds ON feeds.id = items.feed_id")
3334
if filter.Keyword != nil {
3435
expr := "%" + *filter.Keyword + "%"
3536
db = db.Where("title LIKE ? OR content LIKE ?", expr, expr)
3637
}
3738
if filter.FeedID != nil {
3839
db = db.Where("feed_id = ?", *filter.FeedID)
3940
}
41+
if filter.GroupID != nil {
42+
db = db.Where("feeds.group_id = ?", *filter.GroupID)
43+
}
4044
if filter.Unread != nil {
4145
db = db.Where("unread = ?", *filter.Unread)
4246
}
@@ -48,7 +52,7 @@ func (i Item) List(filter ItemFilter, page, pageSize int) ([]*model.Item, int, e
4852
return nil, 0, err
4953
}
5054

51-
err = db.Joins("Feed").Order("items.pub_date desc, items.created_at desc").
55+
err = db.Preload("Feed").Order("items.pub_date desc, items.created_at desc").
5256
Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error
5357
return res, int(total), err
5458
}

server/item.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (i Item) List(ctx context.Context, req *ReqItemList) (*RespItemList, error)
2929
filter := repo.ItemFilter{
3030
Keyword: req.Keyword,
3131
FeedID: req.FeedID,
32+
GroupID: req.GroupID,
3233
Unread: req.Unread,
3334
Bookmark: req.Bookmark,
3435
}

server/item_form.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ReqItemList struct {
2525
Paginate
2626
Keyword *string `query:"keyword"`
2727
FeedID *uint `query:"feed_id"`
28+
GroupID *uint `query:"group_id"`
2829
Unread *bool `query:"unread"`
2930
Bookmark *bool `query:"bookmark"`
3031
}

0 commit comments

Comments
 (0)