Skip to content

Commit b43a177

Browse files
committed
feat: add bookmark
1 parent 5dee1de commit b43a177

File tree

14 files changed

+126
-71
lines changed

14 files changed

+126
-71
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ cd build
5555

5656
## ToDo
5757

58-
- Bookmark
5958
- PWA
6059

6160
## Credits

api/api.go

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ func Run() {
3232
if conf.Debug {
3333
r.Debug = true
3434
r.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) {
35+
if len(resBody) > 500 {
36+
resBody = append(resBody[:500], []byte("...")...)
37+
}
3538
r.Logger.Debugf("req: %s\nresp: %s\n", reqBody, resBody)
3639
}))
3740
}

frontend/src/lib/api/item.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type listOptions = {
77
keyword?: string;
88
feed_id?: number;
99
unread?: boolean;
10+
bookmark?: boolean;
1011
};
1112

1213
export async function listItems(options?: listOptions) {
@@ -21,10 +22,14 @@ export async function getItem(id: number) {
2122
return api.get('items/' + id).json<Item>();
2223
}
2324

24-
export async function updateItem(id: number, unread: boolean) {
25+
export async function updateItem(
26+
id: number,
27+
data: {
28+
unread?: boolean;
29+
bookmark?: boolean;
30+
}
31+
) {
2532
return api.patch('items/' + id, {
26-
json: {
27-
unread: unread
28-
}
33+
json: data
2934
});
3035
}

frontend/src/lib/api/model.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export type Item = {
2020
content: string;
2121
pub_date: Date;
2222
unread: boolean;
23+
bookmark: boolean;
2324
feed: { id: number; name: string };
2425
};

frontend/src/lib/components/ItemAction.svelte

+35-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<script lang="ts">
2-
import { CheckIcon, ExternalLinkIcon, UndoIcon } from 'lucide-svelte';
2+
import {
3+
BookmarkIcon,
4+
BookmarkXIcon,
5+
CheckIcon,
6+
ExternalLinkIcon,
7+
UndoIcon
8+
} from 'lucide-svelte';
39
import type { ComponentType } from 'svelte';
410
import type { Icon } from 'lucide-svelte';
511
import * as Tooltip from '$lib/components/ui/tooltip';
@@ -12,47 +18,53 @@
1218
id: number;
1319
link: string;
1420
unread: boolean;
21+
bookmark: boolean;
1522
};
1623
1724
function getActions(
18-
unread: boolean
25+
unread: boolean,
26+
bookmark: boolean
1927
): { icon: ComponentType<Icon>; tooltip: string; handler: (e: Event) => void }[] {
20-
const list = [
21-
// { icon: BookmarkIcon, tooltip: 'Save to Bookmark', handler: handleSaveToBookmark },
22-
{ icon: ExternalLinkIcon, tooltip: 'Visit Original Link', handler: handleExternalLink }
23-
];
28+
const visitOriginalAction = {
29+
icon: ExternalLinkIcon,
30+
tooltip: 'Visit Original Link',
31+
handler: handleExternalLink
32+
};
2433
const unreadAction = unread
25-
? { icon: CheckIcon, tooltip: 'Mark as Read', handler: handleMarkAsRead }
26-
: { icon: UndoIcon, tooltip: 'Mark as Unread', handler: handleMarkAsUnread };
27-
list.unshift(unreadAction);
28-
return list;
34+
? { icon: CheckIcon, tooltip: 'Mark as Read', handler: handleToggleUnread }
35+
: { icon: UndoIcon, tooltip: 'Mark as Unread', handler: handleToggleUnread };
36+
const bookmarkAction = bookmark
37+
? { icon: BookmarkXIcon, tooltip: 'Cancel Bookmark', handler: handleToggleBookmark }
38+
: { icon: BookmarkIcon, tooltip: 'Add to Bookmark', handler: handleToggleBookmark };
39+
40+
return [unreadAction, bookmarkAction, visitOriginalAction];
2941
}
30-
$: actions = getActions(data.unread);
42+
$: actions = getActions(data.unread, data.bookmark);
3143
32-
async function handleMarkAsRead(e: Event) {
44+
async function handleToggleUnread(e: Event) {
3345
e.preventDefault();
3446
try {
35-
await updateItem(data.id, false);
47+
await updateItem(data.id, { unread: !data.unread });
48+
invalidateAll();
3649
} catch (e) {
3750
toast.error((e as Error).message);
3851
}
39-
invalidateAll();
4052
}
4153
42-
async function handleMarkAsUnread(e: Event) {
54+
function handleExternalLink(e: Event) {
55+
e.preventDefault();
56+
handleToggleUnread(e);
57+
window.open(data.link, '_target');
58+
}
59+
60+
async function handleToggleBookmark(e: Event) {
4361
e.preventDefault();
4462
try {
45-
await updateItem(data.id, true);
63+
await updateItem(data.id, { bookmark: !data.bookmark });
64+
invalidateAll();
4665
} catch (e) {
4766
toast.error((e as Error).message);
4867
}
49-
invalidateAll();
50-
}
51-
52-
function handleExternalLink(e: Event) {
53-
e.preventDefault();
54-
handleMarkAsRead(e);
55-
window.open(data.link, '_target');
5668
}
5769
</script>
5870

frontend/src/lib/components/ItemList.svelte

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@
6060
</div>
6161

6262
<div class="w-full hidden group-hover:inline-flex justify-end">
63-
<ItemAction data={{ id: item.id, link: item.link, unread: item.unread }} />
63+
<ItemAction
64+
data={{ id: item.id, link: item.link, unread: item.unread, bookmark: item.bookmark }}
65+
/>
6466
</div>
6567
</div>
6668
</Button>

frontend/src/lib/components/Navbar.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
}
1010
let links: link[] = [
1111
{ label: 'Unread', url: '/' },
12+
{ label: 'Bookmark', url: '/bookmarks' },
1213
{ label: 'All', url: '/all' },
1314
{ label: 'Feeds', url: '/feeds' }
1415
];
@@ -31,7 +32,7 @@
3132

3233
<nav class="block w-full sm:mt-3 mb-6">
3334
<div
34-
class="flex justify-around items-center w-full sm:max-w-sm mx-auto px-6 py-4 sm:rounded-2xl shadow-md sm:border bg-background"
35+
class="flex justify-around items-center w-full sm:max-w-[600px] mx-auto px-6 py-4 sm:rounded-2xl shadow-md sm:border bg-background"
3536
>
3637
<div class="flex items-center">
3738
<img src="/favicon.png" alt="logo" class="w-10" />
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import type { PageData } from './$types';
3+
import ItemList from '$lib/components/ItemList.svelte';
4+
import PageHead from '$lib/components/PageHead.svelte';
5+
6+
export let data: PageData;
7+
</script>
8+
9+
<svelte:head>
10+
<title>Bookmark</title>
11+
</svelte:head>
12+
13+
<PageHead title="Bookmark" />
14+
<ItemList data={data.items} />
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { listItems } from '$lib/api/item';
2+
import type { PageLoad } from './$types';
3+
4+
export const load: PageLoad = () => {
5+
return listItems({ bookmark: true });
6+
};

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 }} />
60+
<ItemAction data={{ id: data.id, link: data.link, unread: data.unread, bookmark: data.bookmark }} />
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}

model/item.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ type Item struct {
1212
UpdatedAt time.Time
1313
DeletedAt soft_delete.DeletedAt
1414

15-
Title *string `gorm:"title;not null;index"`
16-
GUID *string `gorm:"guid,index"`
17-
Link *string `gorm:"link,index"`
18-
Content *string `gorm:"content"`
19-
PubDate *time.Time `gorm:"pub_date"`
20-
Unread *bool `gorm:"unread;default:true"`
15+
Title *string `gorm:"title;not null;index"`
16+
GUID *string `gorm:"guid,index"`
17+
Link *string `gorm:"link,index"`
18+
Content *string `gorm:"content"`
19+
PubDate *time.Time `gorm:"pub_date"`
20+
Unread *bool `gorm:"unread;default:true;index"`
21+
Bookmark *bool `gorm:"bookmark;default:false;index"`
2122

2223
FeedID uint `gorm:"feed_id;index"`
2324
Feed Feed

repo/item.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ type Item struct {
1818
}
1919

2020
type ItemFilter struct {
21-
Keyword *string
22-
FeedID *uint
23-
Unread *bool
21+
Keyword *string
22+
FeedID *uint
23+
Unread *bool
24+
Bookmark *bool
2425
}
2526

2627
func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, error) {
@@ -35,7 +36,10 @@ func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, e
3536
db = db.Where("feed_id = ?", *filter.FeedID)
3637
}
3738
if filter.Unread != nil {
38-
db = db.Where("unread = true")
39+
db = db.Where("unread = ?", *filter.Unread)
40+
}
41+
if filter.Bookmark != nil {
42+
db = db.Where("bookmark = ?", *filter.Bookmark)
3943
}
4044
err := db.Count(&total).Error
4145
if err != nil {

server/item.go

+21-17
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ func NewItem(repo ItemRepo) *Item {
2626

2727
func (i Item) List(req *ReqItemList) (*RespItemList, error) {
2828
filter := repo.ItemFilter{
29-
Keyword: req.Keyword,
30-
FeedID: req.FeedID,
31-
Unread: req.Unread,
29+
Keyword: req.Keyword,
30+
FeedID: req.FeedID,
31+
Unread: req.Unread,
32+
Bookmark: req.Bookmark,
3233
}
3334
data, total, err := i.repo.List(filter, req.Offset, req.Count)
3435
if err != nil {
@@ -38,12 +39,13 @@ func (i Item) List(req *ReqItemList) (*RespItemList, error) {
3839
items := make([]*ItemForm, 0, len(data))
3940
for _, v := range data {
4041
items = append(items, &ItemForm{
41-
ID: v.ID,
42-
GUID: v.GUID,
43-
Title: v.Title,
44-
Link: v.Link,
45-
PubDate: v.PubDate,
46-
Unread: v.Unread,
42+
ID: v.ID,
43+
GUID: v.GUID,
44+
Title: v.Title,
45+
Link: v.Link,
46+
PubDate: v.PubDate,
47+
Unread: v.Unread,
48+
Bookmark: v.Bookmark,
4749
Feed: ItemFeed{
4850
ID: v.Feed.ID,
4951
Name: v.Feed.Name,
@@ -63,13 +65,14 @@ func (i Item) Get(req *ReqItemGet) (*RespItemGet, error) {
6365
}
6466

6567
return &RespItemGet{
66-
ID: data.ID,
67-
GUID: data.GUID,
68-
Title: data.Title,
69-
Link: data.Link,
70-
Content: data.Content,
71-
PubDate: data.PubDate,
72-
Unread: data.Unread,
68+
ID: data.ID,
69+
GUID: data.GUID,
70+
Title: data.Title,
71+
Link: data.Link,
72+
Content: data.Content,
73+
PubDate: data.PubDate,
74+
Unread: data.Unread,
75+
Bookmark: data.Bookmark,
7376
Feed: ItemFeed{
7477
ID: data.Feed.ID,
7578
Name: data.Feed.Name,
@@ -79,7 +82,8 @@ func (i Item) Get(req *ReqItemGet) (*RespItemGet, error) {
7982

8083
func (i Item) Update(req *ReqItemUpdate) error {
8184
return i.repo.Update(req.ID, &model.Item{
82-
Unread: req.Unread,
85+
Unread: req.Unread,
86+
Bookmark: req.Bookmark,
8387
})
8488
}
8589

server/item_form.go

+16-13
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ type ItemFeed struct {
77
Name *string `json:"name"`
88
}
99
type ItemForm struct {
10-
ID uint `json:"id"`
11-
Title *string `json:"title"`
12-
Link *string `json:"link"`
13-
GUID *string `json:"guid"`
14-
Content *string `json:"content"`
15-
PubDate *time.Time `json:"pub_date"`
16-
Unread *bool `json:"unread"`
17-
Feed ItemFeed `json:"feed"`
10+
ID uint `json:"id"`
11+
Title *string `json:"title"`
12+
Link *string `json:"link"`
13+
GUID *string `json:"guid"`
14+
Content *string `json:"content"`
15+
PubDate *time.Time `json:"pub_date"`
16+
Unread *bool `json:"unread"`
17+
Bookmark *bool `json:"bookmark"`
18+
Feed ItemFeed `json:"feed"`
1819
}
1920

2021
type ReqItemList struct {
2122
Paginate
22-
Keyword *string `query:"keyword"`
23-
FeedID *uint `query:"feed_id"`
24-
Unread *bool `query:"unread"`
23+
Keyword *string `query:"keyword"`
24+
FeedID *uint `query:"feed_id"`
25+
Unread *bool `query:"unread"`
26+
Bookmark *bool `query:"bookmark"`
2527
}
2628

2729
type RespItemList struct {
@@ -36,8 +38,9 @@ type ReqItemGet struct {
3638
type RespItemGet ItemForm
3739

3840
type ReqItemUpdate struct {
39-
ID uint `param:"id" validate:"required"`
40-
Unread *bool `json:"unread"`
41+
ID uint `param:"id" validate:"required"`
42+
Unread *bool `json:"unread"`
43+
Bookmark *bool `json:"bookmark"`
4144
}
4245

4346
type ReqItemDelete struct {

0 commit comments

Comments
 (0)