diff --git a/README.md b/README.md index 1cf1624..99cc626 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/frontend/src/lib/api/item.ts b/frontend/src/lib/api/item.ts index a636bc6..0c20cdf 100644 --- a/frontend/src/lib/api/item.ts +++ b/frontend/src/lib/api/item.ts @@ -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(); } diff --git a/frontend/src/lib/components/ItemAction.svelte b/frontend/src/lib/components/ItemAction.svelte index 38256bd..d945e2f 100644 --- a/frontend/src/lib/components/ItemAction.svelte +++ b/frontend/src/lib/components/ItemAction.svelte @@ -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); } diff --git a/frontend/src/lib/components/ItemList.svelte b/frontend/src/lib/components/ItemList.svelte index d58e77f..5d87278 100644 --- a/frontend/src/lib/components/ItemList.svelte +++ b/frontend/src/lib/components/ItemList.svelte @@ -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; tooltip: string; handler: () => void }[] = [ - { icon: CheckCheckIcon, tooltip: 'Mark All As Read', handler: handleMarkAllAsRead } + { icon: CheckCheckIcon, tooltip: 'Mark as Read', handler: handleMarkAllAsRead } ];
- { - 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; - }} - > + - All Feeds {#each allFeeds as feed} - {feed.name} + {feed.label} {/each} - {#if data.length > 0} + {#if data.items.data.length > 0}
{#each actions as action} @@ -103,7 +102,7 @@
    - {#each data as item} + {#each data.items.data as item}