Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/lib/components/EmptyState.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts">
import * as Empty from '$lib/components/ui/empty';
import { cn } from '$lib/utils';
import type { Component, Snippet } from 'svelte';

interface Props {
/** Icon component (e.g. a lucide icon) rendered in the media slot. */
icon: Component;
title: string;
description?: string;
/** `default` fills/centers the available space; `compact` is a small bordered box. */
compact?: boolean;
class?: string;
/** Optional action area (e.g. a button), rendered below the text. */
children?: Snippet;
}

let { icon: Icon, title, description, compact, class: className, children }: Props = $props();
</script>

<Empty.Root class={cn(compact ? 'flex-none rounded-md border' : 'gap-6 py-12 sm:gap-8', className)}>
<Empty.Header class={compact ? undefined : 'gap-3'}>
<Empty.Media
variant="icon"
class={compact
? undefined
: "size-16 rounded-2xl sm:size-20 [&_svg:not([class*='size-'])]:size-8 sm:[&_svg:not([class*='size-'])]:size-10"}
>
<Icon />
</Empty.Media>
<Empty.Title class={compact ? undefined : 'text-xl sm:text-2xl'}>{title}</Empty.Title>
{#if description}
<Empty.Description class={compact ? undefined : 'text-base sm:text-lg'}>
{description}
</Empty.Description>
{/if}
</Empty.Header>
{#if children}
<Empty.Content>
{@render children()}
</Empty.Content>
{/if}
</Empty.Root>
23 changes: 23 additions & 0 deletions src/lib/components/ui/empty/empty-content.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/shadcn.js";
import type { HTMLAttributes } from "svelte/elements";

let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
data-slot="empty-content"
class={cn(
"gap-2.5 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance",
className
)}
{...restProps}
>
{@render children?.()}
</div>
23 changes: 23 additions & 0 deletions src/lib/components/ui/empty/empty-description.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/shadcn.js";
import type { HTMLAttributes } from "svelte/elements";

let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
data-slot="empty-description"
class={cn(
"text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...restProps}
>
{@render children?.()}
</div>
20 changes: 20 additions & 0 deletions src/lib/components/ui/empty/empty-header.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/shadcn.js";
import type { HTMLAttributes } from "svelte/elements";

let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
data-slot="empty-header"
class={cn("gap-2 flex max-w-sm flex-col items-center", className)}
{...restProps}
>
{@render children?.()}
</div>
41 changes: 41 additions & 0 deletions src/lib/components/ui/empty/empty-media.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";

export const emptyMediaVariants = tv({
base: "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-4",
},
},
defaultVariants: {
variant: "default",
},
});

export type EmptyMediaVariant = VariantProps<typeof emptyMediaVariants>["variant"];
</script>

<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/shadcn.js";
import type { HTMLAttributes } from "svelte/elements";

let {
ref = $bindable(null),
class: className,
children,
variant = "default",
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: EmptyMediaVariant } = $props();
</script>

<div
bind:this={ref}
data-slot="empty-icon"
data-variant={variant}
class={cn(emptyMediaVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</div>
20 changes: 20 additions & 0 deletions src/lib/components/ui/empty/empty-title.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/shadcn.js";
import type { HTMLAttributes } from "svelte/elements";

let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
data-slot="empty-title"
class={cn("text-sm font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>
23 changes: 23 additions & 0 deletions src/lib/components/ui/empty/empty.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils/shadcn.js";
import type { HTMLAttributes } from "svelte/elements";

let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
data-slot="empty"
class={cn(
"gap-4 rounded-xl border-dashed p-6 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance",
className
)}
{...restProps}
>
{@render children?.()}
</div>
22 changes: 22 additions & 0 deletions src/lib/components/ui/empty/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
21 changes: 18 additions & 3 deletions src/routes/(app)/admin/users/[userId]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import { adminGetUsers } from '$lib/api';
import type { AdminUsersView, AdminUsersViewPaginated } from '$lib/api';
import Container from '$lib/components/Container.svelte';
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';
import UserX from '@lucide/svelte/icons/user-x';

let user = $state<AdminUsersView | null>(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) {
Expand All @@ -23,12 +27,23 @@
}

$effect(() => {
loading = true;
adminGetUsers({ query: { $filter: 'id eq ' + page.params.userId } })
.then(handleResponse)
.catch(handleApiError);
.catch(handleApiError)
.finally(() => (loading = false));
});
</script>

<Container>
{user?.name ?? 'Loading...'}
{#if loading}
<div class="flex items-center gap-3 p-12">
<Spinner class="size-5" />
<span class="text-muted-foreground">Loading user...</span>
</div>
{:else if !user}
<EmptyState icon={UserX} title="User not found" description="No user found with this ID." />
{:else}
{user.name}
{/if}
</Container>
14 changes: 7 additions & 7 deletions src/routes/(app)/settings/connections/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,7 +112,7 @@
</Card.Description>
</Card.Header>

<Card.Content class="w-full space-y-6">
<Card.Content class="flex w-full flex-1 flex-col space-y-6">
<!-- Quick actions -->
<div class="flex flex-wrap gap-2">
<Dropdown.Root>
Expand Down Expand Up @@ -162,12 +163,11 @@
{/each}
</div>
{:else if connections.length === 0}
<div class="rounded-xl border p-6 text-sm">
<div class="mb-2 font-medium">No connections yet</div>
<div class="text-muted-foreground">
Choose “Link new provider” above to connect available providers.
</div>
</div>
<EmptyState
icon={Link2}
title="No connections yet"
description="Choose “Link new provider” above to connect available providers."
/>
{:else}
<div class="grid gap-3 md:grid-cols-2">
{#each backendMetadata.state!.oAuthProviders as p (p)}
Expand Down
16 changes: 7 additions & 9 deletions src/routes/(app)/shares/public/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -93,19 +94,16 @@
<Spinner class="size-8 text-gray-600 dark:text-gray-300" />
</div>
{:else if sortedShares.length === 0}
<div
class="border-border/60 text-muted-foreground flex w-full flex-col items-center justify-center gap-3 rounded-lg border border-dashed py-16"
<EmptyState
icon={Link2}
title="No public shares yet"
description="Create a link that anyone can use to control your shockers."
>
<Link2 class="size-10 opacity-50" />
<div class="text-center">
<p class="text-foreground text-sm font-medium">No public shares yet</p>
<p class="mt-1 text-xs">Create a link that anyone can use to control your shockers.</p>
</div>
<Button onclick={() => (showAddShareModal = true)} size="sm">
<Button size="lg" onclick={() => (showAddShareModal = true)}>
<Plus />
New Share
</Button>
</div>
</EmptyState>
{:else}
<div
class="grid w-full grid-cols-1 gap-4 sm:[grid-template-columns:repeat(auto-fill,minmax(18rem,1fr))]"
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/shares/user/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
</Tabs.Root>
</div>

<div class="flex w-full flex-col space-y-4 overflow-auto">
<div class="flex w-full flex-1 flex-col space-y-4 overflow-auto">
{@render children?.()}
</div>
</Container>
28 changes: 19 additions & 9 deletions src/routes/(app)/shares/user/incoming/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script lang="ts">
import EmptyState from '$lib/components/EmptyState.svelte';
import { Spinner } from '$lib/components/ui/spinner';
import * as Table from '$lib/components/ui/table';
import { registerBreadcrumbs } from '$lib/state/breadcrumbs-state.svelte';
import { userSharesState, refreshUserShares } from '$lib/state/user-shares-state.svelte';
import Inbox from '@lucide/svelte/icons/inbox';
import IncomingShareItem from './incoming-share-item.svelte';
import ManageShare from './manage-share.svelte';

Expand Down Expand Up @@ -32,15 +34,23 @@
<Spinner class="size-8 text-gray-600 dark:text-gray-300" />
</div>
{:then}
<div class="mb-6 overflow-y-auto rounded-md border">
<Table.Root>
<Table.Body>
{#each userSharesState.shares.incoming as incomingShare, i (incomingShare.id)}
<IncomingShareItem share={incomingShare} onOpenEdit={() => openManageDrawer(i)} />
{/each}
</Table.Body>
</Table.Root>
</div>
{#if userSharesState.shares.incoming.length === 0}
<EmptyState
icon={Inbox}
title="Nothing shared with you"
description="No one has shared a shocker with you yet."
/>
{:else}
<div class="mb-6 overflow-y-auto rounded-md border">
<Table.Root>
<Table.Body>
{#each userSharesState.shares.incoming as incomingShare, i (incomingShare.id)}
<IncomingShareItem share={incomingShare} onOpenEdit={() => openManageDrawer(i)} />
{/each}
</Table.Body>
</Table.Root>
</div>
{/if}
{:catch error}
<div class="text-red-500">Failed to load outgoing invites: {error.message}</div>
{/await}
Loading
Loading