1
0
Fork 1
mirror of https://github.com/0x2E/fusion.git synced 2025-06-10 18:10:57 +09:00

refactor: fetch item-list data by sveltekit load

This commit is contained in:
rook1e 2024-03-14 00:15:39 +08:00
parent c66559ac82
commit 79345d5f4e
No known key found for this signature in database
GPG key ID: C63289D731719BC0
13 changed files with 148 additions and 78 deletions

View file

@ -53,12 +53,8 @@ cd build
./fusion
```
## ToDo
- PWA
## Credits
- Frontend is built with: [Sveltekit](https://github.com/sveltejs/kit), [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte)
- Backend is built with: [Echo framework](https://github.com/labstack/echo), [GORM](https://github.com/go-gorm/gorm)
- Backend is built with: [Echo](https://github.com/labstack/echo), [GORM](https://github.com/go-gorm/gorm)
- Parsing feed with [gofeed](https://github.com/mmcdole/gofeed)

View file

@ -2,8 +2,8 @@ import { api } from './api';
import type { Item } from './model';
export type ListFilter = {
count?: number;
offset?: number;
page: number;
page_size: number;
keyword?: string;
feed_id?: number;
unread?: boolean;
@ -22,6 +22,23 @@ export async function listItems(options?: ListFilter) {
.json<{ total: number; items: Item[] }>();
}
export function parseURLtoFilter(params: URLSearchParams) {
const filter: ListFilter = {
page: parseInt(params.get('page') || '1'),
page_size: parseInt(params.get('page_size') || '10')
};
const keyword = params.get('keyword');
if (keyword) filter.keyword = keyword;
const feed_id = params.get('feed_id');
if (feed_id) filter.feed_id = parseInt(feed_id);
const unread = params.get('unread');
if (unread) filter.unread = unread === 'true';
const bookmark = params.get('bookmark');
if (bookmark) filter.bookmark = bookmark === 'true';
console.log(JSON.stringify(filter));
return filter;
}
export async function getItem(id: number) {
return api.get('items/' + id).json<Item>();
}

View file

@ -13,6 +13,7 @@
import { toast } from 'svelte-sonner';
import type { Item } from '$lib/api/model';
import { updateBookmark, updateUnread } from '$lib/api/item';
import { invalidateAll } from '$app/navigation';
export let data: Item;
@ -36,12 +37,11 @@
}
$: actions = getActions(data.unread, data.bookmark);
// TODO: use invalidateAll after refactoring ItemAction's parents with page load
async function handleToggleUnread(e: Event) {
e.preventDefault();
try {
await updateUnread([data.id], !data.unread);
data.unread = !data.unread;
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}
@ -51,7 +51,7 @@
e.preventDefault();
try {
await updateBookmark(data.id, !data.bookmark);
data.bookmark = !data.bookmark;
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}

View file

@ -6,85 +6,84 @@
import * as Tooltip from '$lib/components/ui/tooltip';
import * as Pagination from '$lib/components/ui/pagination';
import type { Feed, Item } from '$lib/api/model';
import { listItems, type ListFilter, updateUnread } from '$lib/api/item';
import { type ListFilter, updateUnread, parseURLtoFilter } from '$lib/api/item';
import { toast } from 'svelte-sonner';
import { allFeeds as fetchAllFeeds } from '$lib/api/feed';
import type { ComponentType } from 'svelte';
import { type ComponentType } from 'svelte';
import { CheckCheckIcon, type Icon } from 'lucide-svelte';
import { page } from '$app/stores';
import { goto, invalidateAll } from '$app/navigation';
export let filter: ListFilter = { offset: 0, count: 10 };
export let data: { feeds: Feed[]; items: { total: number; data: Item[] } };
data.items.data = data.items.data.sort(
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
);
const filter = parseURLtoFilter($page.url.searchParams);
if (filter.offset === undefined) filter.offset = 0;
if (filter.count === undefined) filter.count = 10;
fetchAllFeeds()
.then((v) => {
allFeeds = v;
type feedOption = { label: string; value: number };
const defaultSelectedFeed: feedOption = { value: -1, label: 'All Feeds' };
const allFeeds: feedOption[] = data.feeds
.map((f) => {
return { value: f.id, label: f.name };
})
.catch((e) => {
toast.error('Failed to fetch feeds data: ' + e);
});
.concat(defaultSelectedFeed)
.sort((a, b) => a.value - b.value);
let selectedFeed = allFeeds.find((v) => v.value === filter.feed_id) || defaultSelectedFeed;
let data: Item[] = [];
let allFeeds: Feed[] = [];
let currentPage = 1;
let total = 0;
let currentPage = filter.page;
$: filter.offset = (currentPage - 1) * (filter?.count || 10);
$: fetchItems(filter);
$: updateSelectedFeed(selectedFeed);
function updateSelectedFeed(f: feedOption) {
console.log(f);
filter.feed_id = f.value !== -1 ? f.value : undefined;
filter.page = 1;
setURLSearchParams(filter);
}
async function fetchItems(filter: ListFilter) {
try {
const resp = await listItems(filter);
data = resp.items.sort(
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
);
total = resp.total;
} catch (e) {
toast.error((e as Error).message);
$: updatePage(currentPage);
function updatePage(p: number) {
filter.page = p;
setURLSearchParams(filter);
}
function setURLSearchParams(f: ListFilter) {
const p = new URLSearchParams($page.url.searchParams);
for (let key in f) {
p.delete(key);
if (f[key] !== undefined) {
p.set(key, String(f[key]));
}
}
goto('?' + p.toString());
}
async function handleMarkAllAsRead() {
try {
const ids = data.map((v) => v.id);
const ids = data.items.data.map((v) => v.id);
await updateUnread(ids, false);
toast.success('Update successfully');
data.forEach((v) => (v.unread = false));
data = data;
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}
}
const actions: { icon: ComponentType<Icon>; tooltip: string; handler: () => void }[] = [
{ icon: CheckCheckIcon, tooltip: 'Mark All As Read', handler: handleMarkAllAsRead }
{ icon: CheckCheckIcon, tooltip: 'Mark as Read', handler: handleMarkAllAsRead }
];
</script>
<div class="flex justify-between items-center w-full">
<Select.Root
items={allFeeds.map((v) => {
return { value: v.id.toString(), label: v.name };
})}
onSelectedChange={(v) => {
if (!v) return;
const feedID = parseInt(v.value);
filter.feed_id = feedID > 0 ? feedID : undefined;
filter.offset = 0;
}}
>
<Select.Root items={allFeeds} bind:selected={selectedFeed}>
<Select.Trigger class="w-[180px]">
<Select.Value placeholder="Filter by Feed" />
</Select.Trigger>
<Select.Content class="max-h-40 overflow-scroll">
<Select.Item value="all">All Feeds</Select.Item>
{#each allFeeds as feed}
<Select.Item value={feed.id}>{feed.name}</Select.Item>
<Select.Item value={feed.value}>{feed.label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{#if data.length > 0}
{#if data.items.data.length > 0}
<div>
{#each actions as action}
<Tooltip.Root>
@ -103,7 +102,7 @@
</div>
<ul class="mt-4">
{#each data as item}
{#each data.items.data as item}
<li class="group rounded-md">
<Button
href={'/items?id=' + item.id}
@ -132,10 +131,10 @@
{/each}
</ul>
{#if total > (filter?.count || 10)}
{#if data.items.total > filter.page_size}
<Pagination.Root
count={total}
perPage={filter.count}
count={data.items.total}
perPage={filter.page_size}
bind:page={currentPage}
let:pages
let:currentPage

View file

@ -1,6 +1,9 @@
<script lang="ts">
import ItemList from '$lib/components/ItemList.svelte';
import PageHead from '$lib/components/PageHead.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
@ -8,4 +11,4 @@
</svelte:head>
<PageHead title="Unread" />
<ItemList filter={{ unread: true }} />
<ItemList {data} />

View file

@ -1,6 +1,18 @@
import { listItems } from '$lib/api/item';
import { allFeeds } from '$lib/api/feed';
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = () => {
return listItems({ unread: true });
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams);
filter.unread = true;
filter.bookmark = undefined;
const feeds = await allFeeds();
const items = await listItems(filter);
return {
feeds: feeds,
items: {
total: items.total,
data: items.items
}
};
};

View file

@ -1,6 +1,9 @@
<script lang="ts">
import ItemList from '$lib/components/ItemList.svelte';
import PageHead from '$lib/components/PageHead.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
@ -8,4 +11,4 @@
</svelte:head>
<PageHead title="All" />
<ItemList filter={{}} />
<ItemList {data} />

View file

@ -0,0 +1,18 @@
import { allFeeds } from '$lib/api/feed';
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams);
filter.unread = undefined;
filter.bookmark = undefined;
const feeds = await allFeeds();
const items = await listItems(filter);
return {
feeds: feeds,
items: {
total: items.total,
data: items.items
}
};
};

View file

@ -1,6 +1,9 @@
<script lang="ts">
import ItemList from '$lib/components/ItemList.svelte';
import PageHead from '$lib/components/PageHead.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
@ -8,4 +11,4 @@
</svelte:head>
<PageHead title="Bookmark" />
<ItemList filter={{ bookmark: true }} />
<ItemList {data} />

View file

@ -0,0 +1,18 @@
import { allFeeds } from '$lib/api/feed';
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams);
filter.unread = undefined;
filter.bookmark = true;
const feeds = await allFeeds();
const items = await listItems(filter);
return {
feeds: feeds,
items: {
total: items.total,
data: items.items
}
};
};

View file

@ -24,7 +24,7 @@ type ItemFilter struct {
Bookmark *bool
}
func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, error) {
func (i Item) List(filter ItemFilter, page, pageSize int) ([]*model.Item, int, error) {
var total int64
var res []*model.Item
db := i.db.Model(&model.Item{})
@ -46,13 +46,8 @@ func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, e
return nil, 0, err
}
if offset != nil {
db = db.Offset(*offset)
}
if count != nil {
db = db.Limit(*count)
}
err = db.Order("items.created_at desc").Joins("Feed").Find(&res).Error
err = db.Order("items.created_at desc").Joins("Feed").
Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error
return res, int(total), err
}

View file

@ -1,6 +1,6 @@
package server
type Paginate struct {
Count *int `query:"count" validate:"omitnil,min=0"`
Offset *int `query:"offset" validate:"omitnil,min=0"`
PageSize int `query:"page_size" validate:"omitnil,min=0"`
Page int `query:"page" validate:"omitnil,min=0"`
}

View file

@ -8,7 +8,7 @@ import (
//go:generate mockgen -destination=item_mock.go -source=item.go -package=server
type ItemRepo interface {
List(filter repo.ItemFilter, offset, count *int) ([]*model.Item, int, error)
List(filter repo.ItemFilter, page, pageSize int) ([]*model.Item, int, error)
Get(id uint) (*model.Item, error)
Delete(id uint) error
UpdateUnread(ids []uint, unread *bool) error
@ -32,7 +32,13 @@ func (i Item) List(req *ReqItemList) (*RespItemList, error) {
Unread: req.Unread,
Bookmark: req.Bookmark,
}
data, total, err := i.repo.List(filter, req.Offset, req.Count)
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 10
}
data, total, err := i.repo.List(filter, req.Page, req.PageSize)
if err != nil {
return nil, err
}