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:
parent
c66559ac82
commit
79345d5f4e
13 changed files with 148 additions and 78 deletions
|
@ -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)
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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} />
|
||||
|
|
18
frontend/src/routes/all/+page.ts
Normal file
18
frontend/src/routes/all/+page.ts
Normal 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
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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} />
|
||||
|
|
18
frontend/src/routes/bookmarks/+page.ts
Normal file
18
frontend/src/routes/bookmarks/+page.ts
Normal 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
|
||||
}
|
||||
};
|
||||
};
|
11
repo/item.go
11
repo/item.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue