Skip to content

Commit 334a805

Browse files
committed
feat(ui): add async navbar with corresponding API
1 parent 6b8065e commit 334a805

23 files changed

+554
-1
lines changed

src/features/User/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createQuery } from '@tanstack/svelte-query';
2+
import { backend, type QueryResponse } from '$shared/api';
3+
import type { IUserCredentials } from './types';
4+
5+
export const session = (): QueryResponse<IUserCredentials> => {
6+
return createQuery({
7+
queryKey: ['user', 'session'],
8+
queryFn: async () => {
9+
return await fetch(backend('/session')).then(
10+
(data) => data.json() as unknown as IUserCredentials,
11+
);
12+
},
13+
});
14+
};

src/features/User/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './api';
2+
export * from './types';

src/features/User/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface IUserCredentials {
2+
id: number;
3+
email: string;
4+
firstName: string;
5+
lastName: string;
6+
avatarURL: string;
7+
}
8+
9+
export interface IUser extends IUserCredentials {}

src/routes/+layout.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
import { queryClient } from '$shared/api';
33
import { QueryClientProvider } from '@tanstack/svelte-query';
44
import { ModeWatcher } from 'mode-watcher';
5+
import { Navbar } from '$widgets/Navbar';
56
import '../app.css';
67
78
let { children } = $props();
89
</script>
910

1011
<ModeWatcher />
1112
<QueryClientProvider client={queryClient}>
12-
<main class="container my-8">
13+
<Navbar />
14+
<main class="container py-8 border-x-[1px] border-border">
1315
{@render children()}
1416
</main>
1517
</QueryClientProvider>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import { Avatar as AvatarPrimitive } from 'bits-ui';
3+
import { cn } from '$shared/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
...restProps
9+
}: AvatarPrimitive.FallbackProps = $props();
10+
</script>
11+
12+
<AvatarPrimitive.Fallback
13+
bind:ref
14+
class={cn('bg-muted flex size-full items-center justify-center', className)}
15+
{...restProps} />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import { Avatar as AvatarPrimitive } from 'bits-ui';
3+
import { cn } from '$shared/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
...restProps
9+
}: AvatarPrimitive.ImageProps = $props();
10+
</script>
11+
12+
<AvatarPrimitive.Image
13+
bind:ref
14+
class={cn('aspect-square size-full', className)}
15+
{...restProps} />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import { Avatar as AvatarPrimitive } from 'bits-ui';
3+
import { cn } from '$shared/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
...restProps
9+
}: AvatarPrimitive.RootProps = $props();
10+
</script>
11+
12+
<AvatarPrimitive.Root
13+
bind:ref
14+
class={cn('relative flex size-10 shrink-0 overflow-hidden rounded-full', className)}
15+
{...restProps} />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Root from './avatar.svelte';
2+
import Image from './avatar-image.svelte';
3+
import Fallback from './avatar-fallback.svelte';
4+
5+
export {
6+
Root,
7+
Image,
8+
Fallback,
9+
//
10+
Root as Avatar,
11+
Image as AvatarImage,
12+
Fallback as AvatarFallback,
13+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
3+
import Check from '@lucide/svelte/icons/check';
4+
import Minus from '@lucide/svelte/icons/minus';
5+
import { cn } from '$shared/utils.js';
6+
import type { Snippet } from 'svelte';
7+
8+
let {
9+
ref = $bindable(null),
10+
checked = $bindable(false),
11+
indeterminate = $bindable(false),
12+
class: className,
13+
children: childrenProp,
14+
...restProps
15+
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
16+
children?: Snippet;
17+
} = $props();
18+
</script>
19+
20+
<DropdownMenuPrimitive.CheckboxItem
21+
bind:ref
22+
bind:checked
23+
bind:indeterminate
24+
class={cn(
25+
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
26+
className,
27+
)}
28+
{...restProps}>
29+
{#snippet children({ checked, indeterminate })}
30+
<span class="absolute left-2 flex size-3.5 items-center justify-center">
31+
{#if indeterminate}
32+
<Minus class="size-4" />
33+
{:else}
34+
<Check class={cn('size-4', !checked && 'text-transparent')} />
35+
{/if}
36+
</span>
37+
{@render childrenProp?.()}
38+
{/snippet}
39+
</DropdownMenuPrimitive.CheckboxItem>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script lang="ts">
2+
import { cn } from '$shared/utils.js';
3+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
4+
5+
let {
6+
ref = $bindable(null),
7+
sideOffset = 4,
8+
portalProps,
9+
class: className,
10+
...restProps
11+
}: DropdownMenuPrimitive.ContentProps & {
12+
portalProps?: DropdownMenuPrimitive.PortalProps;
13+
} = $props();
14+
</script>
15+
16+
<DropdownMenuPrimitive.Portal {...portalProps}>
17+
<DropdownMenuPrimitive.Content
18+
bind:ref
19+
{sideOffset}
20+
class={cn(
21+
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md outline-none',
22+
className,
23+
)}
24+
{...restProps} />
25+
</DropdownMenuPrimitive.Portal>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
3+
import { cn } from '$shared/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
inset,
9+
...restProps
10+
}: DropdownMenuPrimitive.GroupHeadingProps & {
11+
inset?: boolean;
12+
} = $props();
13+
</script>
14+
15+
<DropdownMenuPrimitive.GroupHeading
16+
bind:ref
17+
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
18+
{...restProps} />
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script lang="ts">
2+
import { cn } from '$shared/utils.js';
3+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
inset,
9+
...restProps
10+
}: DropdownMenuPrimitive.ItemProps & {
11+
inset?: boolean;
12+
} = $props();
13+
</script>
14+
15+
<DropdownMenuPrimitive.Item
16+
bind:ref
17+
class={cn(
18+
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
19+
inset && 'pl-8',
20+
className,
21+
)}
22+
{...restProps} />
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script lang="ts">
2+
import { cn } from '$shared/utils.js';
3+
import { type WithElementRef } from 'bits-ui';
4+
import type { HTMLAttributes } from 'svelte/elements';
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
inset,
10+
children,
11+
...restProps
12+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
13+
inset?: boolean;
14+
} = $props();
15+
</script>
16+
17+
<div
18+
bind:this={ref}
19+
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
20+
{...restProps}>
21+
{@render children?.()}
22+
</div>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui';
3+
import Circle from '@lucide/svelte/icons/circle';
4+
import { cn } from '$shared/utils.js';
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
children: childrenProp,
10+
...restProps
11+
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
12+
</script>
13+
14+
<DropdownMenuPrimitive.RadioItem
15+
bind:ref
16+
class={cn(
17+
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
18+
className,
19+
)}
20+
{...restProps}>
21+
{#snippet children({ checked })}
22+
<span class="absolute left-2 flex size-3.5 items-center justify-center">
23+
{#if checked}
24+
<Circle class="size-2 fill-current" />
25+
{/if}
26+
</span>
27+
{@render childrenProp?.({ checked })}
28+
{/snippet}
29+
</DropdownMenuPrimitive.RadioItem>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
3+
import { cn } from '$shared/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
...restProps
9+
}: DropdownMenuPrimitive.SeparatorProps = $props();
10+
</script>
11+
12+
<DropdownMenuPrimitive.Separator
13+
bind:ref
14+
class={cn('bg-muted -mx-1 my-1 h-px', className)}
15+
{...restProps} />
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
import type { HTMLAttributes } from 'svelte/elements';
3+
import { type WithElementRef } from 'bits-ui';
4+
import { cn } from '$shared/utils.js';
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
children,
10+
...restProps
11+
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
12+
</script>
13+
14+
<span
15+
bind:this={ref}
16+
class={cn('ml-auto text-xs tracking-widest opacity-60', className)}
17+
{...restProps}>
18+
{@render children?.()}
19+
</span>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
3+
import { cn } from '$shared/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
...restProps
9+
}: DropdownMenuPrimitive.SubContentProps = $props();
10+
</script>
11+
12+
<DropdownMenuPrimitive.SubContent
13+
bind:ref
14+
class={cn(
15+
'bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none',
16+
className,
17+
)}
18+
{...restProps} />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
3+
import ChevronRight from '@lucide/svelte/icons/chevron-right';
4+
import { cn } from '$shared/utils.js';
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
inset,
10+
children,
11+
...restProps
12+
}: DropdownMenuPrimitive.SubTriggerProps & {
13+
inset?: boolean;
14+
} = $props();
15+
</script>
16+
17+
<DropdownMenuPrimitive.SubTrigger
18+
bind:ref
19+
class={cn(
20+
'data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
21+
inset && 'pl-8',
22+
className,
23+
)}
24+
{...restProps}>
25+
{@render children?.()}
26+
<ChevronRight class="ml-auto" />
27+
</DropdownMenuPrimitive.SubTrigger>

0 commit comments

Comments
 (0)