Skip to content

Commit b83b868

Browse files
authored
Fix UI (#153)
* use invalidateAll() to reduce complexity * remove useless loading placeholder * refactor: update sidebar using global states
1 parent bc8109f commit b83b868

File tree

16 files changed

+110
-128
lines changed

16 files changed

+110
-128
lines changed

frontend/src/lib/components/ItemActionMarkAllasRead.svelte

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script lang="ts">
2-
import { invalidate } from '$app/navigation';
3-
import { page } from '$app/state';
4-
import { listItems, parseURLtoFilter, updateUnread } from '$lib/api/item';
2+
import { invalidateAll } from '$app/navigation';
3+
import { listItems, updateUnread } from '$lib/api/item';
54
import type { Item } from '$lib/api/model';
65
import { t } from '$lib/i18n';
76
import { CheckCheck } from 'lucide-svelte';
@@ -29,7 +28,7 @@
2928
const ids = props.items.map((v) => v.id);
3029
await updateUnread(ids, false);
3130
toast.success(t('state.success'));
32-
invalidate('page:' + page.url.pathname);
31+
invalidateAll();
3332
} catch (e) {
3433
toast.error((e as Error).message);
3534
}
@@ -57,7 +56,7 @@
5756
await updateUnread(ids, false);
5857
}
5958
toast.success(t('state.success'));
60-
invalidate('page:' + page.url.pathname);
59+
invalidateAll();
6160
} catch (e) {
6261
toast.error((e as Error).message);
6362
}

frontend/src/lib/components/ItemActionUnread.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<script module>
2-
import type { Item } from '$lib/api/model';
32
import { updateUnread } from '$lib/api/item';
3+
import type { Item } from '$lib/api/model';
44
import { toast } from 'svelte-sonner';
55
66
export async function toggleUnread(item: Item) {
77
try {
88
await updateUnread([item.id], !item.unread);
99
item.unread = !item.unread;
10+
// we don't refresh the page using invalideAll() because we want to keep the
11+
// modified item in the list rather than be filtered out
12+
updateUnreadCount(item.feed.id, item.unread ? 1 : -1);
1013
} catch (e) {
1114
toast.error((e as Error).message);
1215
}
@@ -15,6 +18,7 @@
1518

1619
<script lang="ts">
1720
import { t } from '$lib/i18n';
21+
import { updateUnreadCount } from '$lib/state.svelte';
1822
import { CheckIcon, UndoIcon } from 'lucide-svelte';
1923
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';
2024

frontend/src/lib/components/Sidebar.svelte

Lines changed: 58 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import { page } from '$app/state';
44
import { getFavicon } from '$lib/api/favicon';
55
import { logout } from '$lib/api/login';
6-
import type { Feed, Group } from '$lib/api/model';
6+
import type { Feed } from '$lib/api/model';
77
import { t } from '$lib/i18n';
8+
import { globalState } from '$lib/state.svelte';
89
import {
910
BookmarkCheck,
1011
ChevronDown,
@@ -28,34 +29,24 @@
2829
} from './ShortcutHelpModal.svelte';
2930
import ThemeController from './ThemeController.svelte';
3031
31-
interface Props {
32-
feeds: Promise<Feed[]>;
33-
groups: Promise<Group[]>;
34-
}
35-
36-
let { feeds, groups }: Props = $props();
37-
38-
// State to track open groups
3932
let openGroups = $state<Record<number, boolean>>({});
4033
41-
let feedList = $derived.by(async () => {
42-
const [feedsData, groupsData] = await Promise.all([feeds, groups]);
34+
let groupList = $derived.by(() => {
4335
const groupFeeds: { id: number; name: string; feeds: (Feed & { indexInList: number })[] }[] =
4436
[];
4537
let curIndexInList = 0;
46-
groupsData.forEach((group) => {
38+
globalState.groups.forEach((group) => {
4739
groupFeeds.push({
4840
id: group.id,
4941
name: group.name,
50-
feeds: feedsData
42+
feeds: globalState.feeds
5143
.filter((feed) => feed.group.id === group.id)
5244
.sort((a, b) => a.name.localeCompare(b.name))
5345
.map((feed) => ({
5446
...feed,
5547
indexInList: curIndexInList++
5648
}))
5749
});
58-
openGroups[group.id] = false;
5950
});
6051
return groupFeeds;
6152
});
@@ -115,15 +106,8 @@
115106
}
116107
117108
let selectedFeedIndex = $state(-1);
118-
let selectedFeedGroupId = $state(-1);
119-
$effect(() => {
120-
feeds.then(() => {
121-
selectedFeedIndex = -1;
122-
selectedFeedGroupId = -1;
123-
});
124-
});
125109
async function moveFeed(direction: 'prev' | 'next') {
126-
const feedList = await feeds;
110+
const feedList = globalState.feeds;
127111
128112
if (feedList.length === 0) return;
129113
@@ -200,60 +184,59 @@
200184

201185
<ul class="menu w-full">
202186
<li class="menu-title text-xs">{t('common.feeds')}</li>
203-
{#await feedList}
204-
<div class="skeleton bg-base-300 h-10"></div>
205-
{:then groupData}
206-
{#each groupData as group (group.id)}
207-
{@const isOpen = openGroups[group.id]}
208-
<li>
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" />
244-
</div>
187+
{#each groupList as group}
188+
{@const isOpen = openGroups[group.id]}
189+
<li class="p-0">
190+
<div class="gap-0 p-0">
191+
<button
192+
class="btn btn-ghost btn-sm btn-square"
193+
onclick={(event) => {
194+
event.preventDefault();
195+
openGroups[group.id] = !isOpen;
196+
}}
197+
>
198+
{#if isOpen}
199+
<ChevronDown class="size-4" />
200+
{:else}
201+
<ChevronRight class="size-4" />
202+
{/if}
203+
</button>
204+
<a
205+
href="/groups/{group.id}"
206+
class="line-clamp-1 block h-full grow place-content-center text-left"
207+
>
208+
{group.name}
209+
</a>
210+
</div>
211+
<ul class:hidden={!isOpen}>
212+
{#each group.feeds as feed}
213+
{@const textColor = feed.suspended
214+
? 'text-neutral-content/60'
215+
: feed.failure
216+
? 'text-error'
217+
: ''}
218+
<li>
219+
<a
220+
id="sidebar-feed-{feed.indexInList}"
221+
data-group-id={group.id}
222+
href="/feeds/{feed.id}"
223+
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
224+
>
225+
<div class="avatar">
226+
<div class="size-4 rounded-full">
227+
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
245228
</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>
254-
</li>
255-
{/each}
256-
{/await}
229+
</div>
230+
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
231+
{#if feed.unread_count > 0}
232+
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
233+
{/if}
234+
</a>
235+
</li>
236+
{/each}
237+
</ul>
238+
</li>
239+
{/each}
257240
</ul>
258241
</div>
259242

frontend/src/lib/state.svelte.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,18 @@ export const globalState = $state({
44
groups: [] as Group[],
55
feeds: [] as Feed[]
66
});
7+
8+
export function setGlobalFeeds(feeds: Feed[]) {
9+
globalState.feeds = feeds;
10+
}
11+
12+
export function setGlobalGroups(groups: Group[]) {
13+
globalState.groups = groups;
14+
}
15+
16+
export function updateUnreadCount(feedId: number, change: number) {
17+
const feed = globalState.feeds.find((f) => f.id === feedId);
18+
if (feed) {
19+
feed.unread_count = Math.max(0, (feed.unread_count || 0) + change);
20+
}
21+
}

frontend/src/routes/(authed)/+layout.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import ShortcutHelpModal from '$lib/components/ShortcutHelpModal.svelte';
55
import Sidebar from '$lib/components/Sidebar.svelte';
66
7-
let { children, data } = $props();
7+
let { children } = $props();
88
let showSidebar = $state(false);
99
beforeNavigate(() => {
1010
showSidebar = false;
@@ -29,7 +29,7 @@
2929
<div
3030
class="text-base-content bg-base-200 z-50 h-full min-h-full w-[80%] overflow-x-hidden px-2 py-4 lg:w-72"
3131
>
32-
<Sidebar feeds={data.feeds} groups={data.groups} />
32+
<Sidebar />
3333
</div>
3434
</div>
3535
</div>
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { listFeeds } from '$lib/api/feed';
22
import { allGroups } from '$lib/api/group';
3-
import { globalState } from '$lib/state.svelte';
3+
import { setGlobalFeeds, setGlobalGroups } from '$lib/state.svelte';
44
import type { LayoutLoad } from './$types';
55

6-
export const load: LayoutLoad = async () => {
7-
const feeds = listFeeds().then((feeds) => {
8-
globalState.feeds = feeds;
9-
return feeds;
10-
});
11-
const groups = allGroups().then((groups) => {
12-
groups.sort((a, b) => a.id - b.id);
13-
globalState.groups = groups;
14-
return groups;
15-
});
16-
return {
17-
feeds,
18-
groups
19-
};
6+
export const load: LayoutLoad = async ({ depends }) => {
7+
depends('app:feeds', 'app:groups');
8+
9+
await Promise.all([
10+
allGroups().then((groups) => {
11+
groups.sort((a, b) => a.id - b.id);
12+
setGlobalGroups(groups);
13+
}),
14+
listFeeds().then((feeds) => {
15+
setGlobalFeeds(feeds);
16+
})
17+
]);
18+
19+
return {};
2020
};

frontend/src/routes/(authed)/+page.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { listItems, parseURLtoFilter } from '$lib/api/item';
22
import type { PageLoad } from './$types';
33

4-
export const load: PageLoad = async ({ depends, url }) => {
5-
depends(`page:${url.pathname}`);
6-
4+
export const load: PageLoad = async ({ url }) => {
75
const filter = parseURLtoFilter(url.searchParams, {
86
unread: true,
97
bookmark: undefined,

frontend/src/routes/(authed)/all/+page.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { listItems, parseURLtoFilter } from '$lib/api/item';
22
import type { PageLoad } from './$types';
33

4-
export const load: PageLoad = async ({ url, depends }) => {
5-
depends(`page:${url.pathname}`);
6-
4+
export const load: PageLoad = async ({ url }) => {
75
const filter = parseURLtoFilter(url.searchParams, {
86
unread: undefined,
97
bookmark: undefined,

frontend/src/routes/(authed)/bookmarks/+page.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { listItems, parseURLtoFilter } from '$lib/api/item';
22
import type { PageLoad } from './$types';
33

4-
export const load: PageLoad = async ({ url, depends }) => {
5-
depends(`page:${url.pathname}`);
6-
4+
export const load: PageLoad = async ({ url }) => {
75
const filter = parseURLtoFilter(url.searchParams, {
86
unread: undefined,
97
bookmark: true,

frontend/src/routes/(authed)/feeds/[id]/+page.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
{/await}
1616
</svelte:head>
1717

18-
{#await data.feed}
19-
Loading...
20-
{:then feed}
18+
{#await data.feed then feed}
2119
<PageNavHeader showSearch={true}>
2220
{#await data.items then items}
2321
<ItemActionMarkAllasRead items={items.items} />

0 commit comments

Comments
 (0)