From 00c0c65d9561999960861acbb159320731c43a0b Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Thu, 18 Jun 2026 12:43:51 +0200 Subject: [PATCH 1/3] fix(ui): add empty and not-found states to share lists and admin user page The hand-rolled share list pages (outgoing/incoming/invites) rendered an empty bordered box when there was nothing to show; add {:else} empty-state copy. The admin user detail page showed 'Loading...' forever on a 404; add proper loading and not-found states. Also fix an 'incomding' typo. --- .../(app)/admin/users/[userId]/+page.svelte | 19 ++++++++++++++++--- .../(app)/shares/user/incoming/+page.svelte | 6 ++++++ .../(app)/shares/user/invites/+page.svelte | 14 +++++++++++++- .../(app)/shares/user/outgoing/+page.svelte | 6 ++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/routes/(app)/admin/users/[userId]/+page.svelte b/src/routes/(app)/admin/users/[userId]/+page.svelte index 2658a0b9..80ca18b1 100644 --- a/src/routes/(app)/admin/users/[userId]/+page.svelte +++ b/src/routes/(app)/admin/users/[userId]/+page.svelte @@ -3,14 +3,16 @@ import { adminGetUsers } from '$lib/api'; import type { AdminUsersView, AdminUsersViewPaginated } from '$lib/api'; import Container from '$lib/components/Container.svelte'; + import { Spinner } from '$lib/components/ui/spinner'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { registerBreadcrumbs } from '$lib/state/breadcrumbs-state.svelte'; let user = $state(null); + let loading = $state(true); registerBreadcrumbs(() => [ { label: 'Users', href: '/admin/users' }, - { label: user?.name ?? 'Loading...' }, + { label: user?.name ?? (loading ? 'Loading...' : 'Not found') }, ]); function handleResponse(page: AdminUsersViewPaginated) { @@ -23,12 +25,23 @@ } $effect(() => { + loading = true; adminGetUsers({ query: { $filter: 'id eq ' + page.params.userId } }) .then(handleResponse) - .catch(handleApiError); + .catch(handleApiError) + .finally(() => (loading = false)); }); - {user?.name ?? 'Loading...'} + {#if loading} +
+ + Loading user... +
+ {:else if !user} +

No user found with this ID.

+ {:else} + {user.name} + {/if}
diff --git a/src/routes/(app)/shares/user/incoming/+page.svelte b/src/routes/(app)/shares/user/incoming/+page.svelte index a7d77a10..f5b15a07 100644 --- a/src/routes/(app)/shares/user/incoming/+page.svelte +++ b/src/routes/(app)/shares/user/incoming/+page.svelte @@ -37,6 +37,12 @@ {#each userSharesState.shares.incoming as incomingShare, i (incomingShare.id)} openManageDrawer(i)} /> + {:else} + + + No one has shared a shocker with you yet. + + {/each} diff --git a/src/routes/(app)/shares/user/invites/+page.svelte b/src/routes/(app)/shares/user/invites/+page.svelte index 53aaeb6e..36875e1c 100644 --- a/src/routes/(app)/shares/user/invites/+page.svelte +++ b/src/routes/(app)/shares/user/invites/+page.svelte @@ -28,6 +28,12 @@ {#each userSharesState.outgoingInvites as invite (invite.id)} + {:else} + + + You have no outgoing share invites. + + {/each} @@ -48,10 +54,16 @@ {#each userSharesState.incomingInvites as invite (invite.id)} + {:else} + + + You have no incoming share invites. + + {/each} {:catch error} -
Failed to load incomding invites: {error.message}
+
Failed to load incoming invites: {error.message}
{/await} diff --git a/src/routes/(app)/shares/user/outgoing/+page.svelte b/src/routes/(app)/shares/user/outgoing/+page.svelte index b98eaf0c..5a79ccf5 100644 --- a/src/routes/(app)/shares/user/outgoing/+page.svelte +++ b/src/routes/(app)/shares/user/outgoing/+page.svelte @@ -36,6 +36,12 @@ {#each userSharesState.shares.outgoing as userShare, i (userShare.id)} openEditDrawer(i)} /> + {:else} + + + You haven't shared any shockers with other users yet. + + {/each} From e2b9fa59a06ee9f24c0e6f6b3f7553bb0a278f01 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Thu, 18 Jun 2026 18:55:10 +0200 Subject: [PATCH 2/3] refactor(ui): use shadcn Empty component for empty/not-found states Replace the hand-rolled empty-state table rows and not-found paragraph with the shadcn-svelte Empty component on the share lists and admin user detail page. The share-list pages now branch on length and render an Empty block instead of an empty bordered table. --- .../components/ui/empty/empty-content.svelte | 23 ++++++ .../ui/empty/empty-description.svelte | 23 ++++++ .../components/ui/empty/empty-header.svelte | 20 +++++ .../components/ui/empty/empty-media.svelte | 41 ++++++++++ .../components/ui/empty/empty-title.svelte | 20 +++++ src/lib/components/ui/empty/empty.svelte | 23 ++++++ src/lib/components/ui/empty/index.ts | 22 ++++++ .../(app)/admin/users/[userId]/+page.svelte | 12 ++- .../(app)/shares/user/incoming/+page.svelte | 38 ++++++---- .../(app)/shares/user/invites/+page.svelte | 75 +++++++++++-------- .../(app)/shares/user/outgoing/+page.svelte | 38 ++++++---- 11 files changed, 274 insertions(+), 61 deletions(-) create mode 100644 src/lib/components/ui/empty/empty-content.svelte create mode 100644 src/lib/components/ui/empty/empty-description.svelte create mode 100644 src/lib/components/ui/empty/empty-header.svelte create mode 100644 src/lib/components/ui/empty/empty-media.svelte create mode 100644 src/lib/components/ui/empty/empty-title.svelte create mode 100644 src/lib/components/ui/empty/empty.svelte create mode 100644 src/lib/components/ui/empty/index.ts diff --git a/src/lib/components/ui/empty/empty-content.svelte b/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 00000000..520ac878 --- /dev/null +++ b/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-description.svelte b/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 00000000..e6d97cde --- /dev/null +++ b/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-header.svelte b/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 00000000..9085f955 --- /dev/null +++ b/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-media.svelte b/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 00000000..6bc65c0f --- /dev/null +++ b/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-title.svelte b/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 00000000..3641794b --- /dev/null +++ b/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty.svelte b/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 00000000..e5f37460 --- /dev/null +++ b/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/index.ts b/src/lib/components/ui/empty/index.ts new file mode 100644 index 00000000..ae4c106e --- /dev/null +++ b/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/src/routes/(app)/admin/users/[userId]/+page.svelte b/src/routes/(app)/admin/users/[userId]/+page.svelte index 80ca18b1..d56897e7 100644 --- a/src/routes/(app)/admin/users/[userId]/+page.svelte +++ b/src/routes/(app)/admin/users/[userId]/+page.svelte @@ -3,9 +3,11 @@ import { adminGetUsers } from '$lib/api'; import type { AdminUsersView, AdminUsersViewPaginated } from '$lib/api'; import Container from '$lib/components/Container.svelte'; + import * as Empty from '$lib/components/ui/empty'; import { Spinner } from '$lib/components/ui/spinner'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { registerBreadcrumbs } from '$lib/state/breadcrumbs-state.svelte'; + import UserX from '@lucide/svelte/icons/user-x'; let user = $state(null); let loading = $state(true); @@ -40,7 +42,15 @@ Loading user... {:else if !user} -

No user found with this ID.

+ + + + + + User not found + No user found with this ID. + + {:else} {user.name} {/if} diff --git a/src/routes/(app)/shares/user/incoming/+page.svelte b/src/routes/(app)/shares/user/incoming/+page.svelte index f5b15a07..f1edc640 100644 --- a/src/routes/(app)/shares/user/incoming/+page.svelte +++ b/src/routes/(app)/shares/user/incoming/+page.svelte @@ -1,8 +1,10 @@ + + + + + + + {title} + {#if description} + + {description} + + {/if} + + {#if children} + + {@render children()} + + {/if} + diff --git a/src/routes/(app)/admin/users/[userId]/+page.svelte b/src/routes/(app)/admin/users/[userId]/+page.svelte index d56897e7..5c9928ed 100644 --- a/src/routes/(app)/admin/users/[userId]/+page.svelte +++ b/src/routes/(app)/admin/users/[userId]/+page.svelte @@ -3,7 +3,7 @@ import { adminGetUsers } from '$lib/api'; import type { AdminUsersView, AdminUsersViewPaginated } from '$lib/api'; import Container from '$lib/components/Container.svelte'; - import * as Empty from '$lib/components/ui/empty'; + import EmptyState from '$lib/components/EmptyState.svelte'; import { Spinner } from '$lib/components/ui/spinner'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { registerBreadcrumbs } from '$lib/state/breadcrumbs-state.svelte'; @@ -42,15 +42,7 @@ Loading user... {:else if !user} - - - - - - User not found - No user found with this ID. - - + {:else} {user.name} {/if} diff --git a/src/routes/(app)/settings/connections/+page.svelte b/src/routes/(app)/settings/connections/+page.svelte index b182939d..2611de00 100644 --- a/src/routes/(app)/settings/connections/+page.svelte +++ b/src/routes/(app)/settings/connections/+page.svelte @@ -10,6 +10,7 @@ import Container from '$lib/components/Container.svelte'; import { Button } from '$lib/components/ui/button'; import * as Card from '$lib/components/ui/card'; + import EmptyState from '$lib/components/EmptyState.svelte'; import * as Dropdown from '$lib/components/ui/dropdown-menu'; import * as Separator from '$lib/components/ui/separator'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; @@ -111,7 +112,7 @@ - +
@@ -162,12 +163,11 @@ {/each}
{:else if connections.length === 0} -
-
No connections yet
-
- Choose “Link new provider” above to connect available providers. -
-
+ {:else}
{#each backendMetadata.state!.oAuthProviders as p (p)} diff --git a/src/routes/(app)/shares/public/+page.svelte b/src/routes/(app)/shares/public/+page.svelte index 039af1c6..1addd18e 100644 --- a/src/routes/(app)/shares/public/+page.svelte +++ b/src/routes/(app)/shares/public/+page.svelte @@ -11,6 +11,7 @@ import type { OwnPublicShareResponse } from '$lib/api'; import Container from '$lib/components/Container.svelte'; import CopyInput from '$lib/components/CopyInput.svelte'; + import EmptyState from '$lib/components/EmptyState.svelte'; import { Spinner } from '$lib/components/ui/spinner'; import Button from '$lib/components/ui/button/button.svelte'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; @@ -93,19 +94,16 @@
{:else if sortedShares.length === 0} -
- -
-

No public shares yet

-

Create a link that anyone can use to control your shockers.

-
- -
+
{:else}
-
+
{@render children?.()}
diff --git a/src/routes/(app)/shares/user/incoming/+page.svelte b/src/routes/(app)/shares/user/incoming/+page.svelte index f1edc640..9d6bcc23 100644 --- a/src/routes/(app)/shares/user/incoming/+page.svelte +++ b/src/routes/(app)/shares/user/incoming/+page.svelte @@ -1,5 +1,5 @@ + let invitesPromise = $state(Promise.all([refreshOutgoingInvites(), refreshIncomingInvites()])); -

Outgoing

+ let bothEmpty = $derived( + userSharesState.outgoingInvites.length === 0 && userSharesState.incomingInvites.length === 0 + ); + -{#await outgoingInvitesPromise} +{#await invitesPromise}
{:then} - {#if userSharesState.outgoingInvites.length === 0} - - - - - - No outgoing invites - You have no outgoing share invites. - - + {#if bothEmpty} + {:else} -
- - - {#each userSharesState.outgoingInvites as invite (invite.id)} - - {/each} - - -
- {/if} -{:catch error} -
Failed to load outgoing invites: {error.message}
-{/await} - -

Incoming

+

Outgoing

+ {#if userSharesState.outgoingInvites.length === 0} + + {:else} +
+ + + {#each userSharesState.outgoingInvites as invite (invite.id)} + + {/each} + + +
+ {/if} -{#await incomingInvitesPromise} -
- -
-{:then} - {#if userSharesState.incomingInvites.length === 0} - - - - - - No incoming invites - You have no incoming share invites. - - - {:else} -
- - - {#each userSharesState.incomingInvites as invite (invite.id)} - - {/each} - - -
+

Incoming

+ {#if userSharesState.incomingInvites.length === 0} + + {:else} +
+ + + {#each userSharesState.incomingInvites as invite (invite.id)} + + {/each} + + +
+ {/if} {/if} {:catch error} -
Failed to load incoming invites: {error.message}
+
Failed to load invites: {error.message}
{/await} diff --git a/src/routes/(app)/shares/user/outgoing/+page.svelte b/src/routes/(app)/shares/user/outgoing/+page.svelte index 6d7dc41c..41efb4ec 100644 --- a/src/routes/(app)/shares/user/outgoing/+page.svelte +++ b/src/routes/(app)/shares/user/outgoing/+page.svelte @@ -1,5 +1,5 @@