chat ui sidebar (#4)
This commit is contained in:
parent
054307caad
commit
4304e435c9
22 changed files with 380 additions and 33 deletions
|
|
@ -47,6 +47,7 @@
|
||||||
"runed": "^0.28.0",
|
"runed": "^0.28.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-toolbelt": "^0.9.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -102,6 +102,9 @@ importers:
|
||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.2.1(picomatch@4.0.2)(svelte@5.34.1)(typescript@5.8.3)
|
version: 4.2.1(picomatch@4.0.2)(svelte@5.34.1)(typescript@5.8.3)
|
||||||
|
svelte-toolbelt:
|
||||||
|
specifier: ^0.9.1
|
||||||
|
version: 0.9.1(svelte@5.34.1)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
|
|
|
||||||
30
src/lib/backend/auth/redirect.ts
Normal file
30
src/lib/backend/auth/redirect.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
const PARAM_NAME = 'redirect_to';
|
||||||
|
|
||||||
|
/** Redirect back to login and add provided url to the redirect_to parameter
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
export function redirectToLogin(url: URL): never {
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
const location = `/login?${PARAM_NAME}=${path}`;
|
||||||
|
|
||||||
|
redirect(303, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Redirect back to an authorized route either using the redirect_to parameter or the fallback if not provided
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* @param fallback
|
||||||
|
*/
|
||||||
|
export function redirectToAuthorized(url: URL, fallback = '/account'): never {
|
||||||
|
const to = getRedirectTo(url) ?? fallback;
|
||||||
|
|
||||||
|
redirect(303, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRedirectTo(url: URL): string | null {
|
||||||
|
return url.searchParams.get(PARAM_NAME);
|
||||||
|
}
|
||||||
18
src/lib/components/icons/github.svelte
Normal file
18
src/lib/components/icons/github.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Props } from '.';
|
||||||
|
import { cn } from '$lib/utils/utils';
|
||||||
|
|
||||||
|
let { class: className, ...rest }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn('size-4 fill-current', className)}
|
||||||
|
viewBox="0 0 256 250"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMidYMid"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
12
src/lib/components/icons/index.ts
Normal file
12
src/lib/components/icons/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import GitHub from './github.svelte';
|
||||||
|
import TypeScript from './typescript.svelte';
|
||||||
|
import Svelte from './svelte.svelte';
|
||||||
|
|
||||||
|
export interface Props extends HTMLAttributes<SVGElement> {
|
||||||
|
class?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GitHub, TypeScript, Svelte };
|
||||||
23
src/lib/components/icons/svelte.svelte
Normal file
23
src/lib/components/icons/svelte.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Props } from '.';
|
||||||
|
import { cn } from '$lib/utils/utils';
|
||||||
|
|
||||||
|
let { class: className, ...rest }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn('size-4', className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 107 128"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<title>svelte-logo</title>
|
||||||
|
<path
|
||||||
|
d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157"
|
||||||
|
style="fill:#ff3e00"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287"
|
||||||
|
style="fill:#fff"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
25
src/lib/components/icons/typescript.svelte
Normal file
25
src/lib/components/icons/typescript.svelte
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Props } from '.';
|
||||||
|
import { cn } from '$lib/utils/utils';
|
||||||
|
|
||||||
|
let { class: className, ...rest }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn('size-4', className)}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...rest}
|
||||||
|
><rect fill="#3178c6" height="512" rx="50" width="512" /><rect
|
||||||
|
fill="#3178c6"
|
||||||
|
height="512"
|
||||||
|
rx="50"
|
||||||
|
width="512"
|
||||||
|
/><path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="m316.939 407.424v50.061c8.138 4.172 17.763 7.3 28.875 9.386s22.823 3.129 35.135 3.129c11.999 0 23.397-1.147 34.196-3.442 10.799-2.294 20.268-6.075 28.406-11.342 8.138-5.266 14.581-12.15 19.328-20.65s7.121-19.007 7.121-31.522c0-9.074-1.356-17.026-4.069-23.857s-6.625-12.906-11.738-18.225c-5.112-5.319-11.242-10.091-18.389-14.315s-15.207-8.213-24.18-11.967c-6.573-2.712-12.468-5.345-17.685-7.9-5.217-2.556-9.651-5.163-13.303-7.822-3.652-2.66-6.469-5.476-8.451-8.448-1.982-2.973-2.974-6.336-2.974-10.091 0-3.441.887-6.544 2.661-9.308s4.278-5.136 7.512-7.118c3.235-1.981 7.199-3.52 11.894-4.615 4.696-1.095 9.912-1.642 15.651-1.642 4.173 0 8.581.313 13.224.938 4.643.626 9.312 1.591 14.008 2.894 4.695 1.304 9.259 2.947 13.694 4.928 4.434 1.982 8.529 4.276 12.285 6.884v-46.776c-7.616-2.92-15.937-5.084-24.962-6.492s-19.381-2.112-31.066-2.112c-11.895 0-23.163 1.278-33.805 3.833s-20.006 6.544-28.093 11.967c-8.086 5.424-14.476 12.333-19.171 20.729-4.695 8.395-7.043 18.433-7.043 30.114 0 14.914 4.304 27.638 12.912 38.172 8.607 10.533 21.675 19.45 39.204 26.751 6.886 2.816 13.303 5.579 19.25 8.291s11.086 5.528 15.415 8.448c4.33 2.92 7.747 6.101 10.252 9.543 2.504 3.441 3.756 7.352 3.756 11.733 0 3.233-.783 6.231-2.348 8.995s-3.939 5.162-7.121 7.196-7.147 3.624-11.894 4.771c-4.748 1.148-10.303 1.721-16.668 1.721-10.851 0-21.597-1.903-32.24-5.71-10.642-3.806-20.502-9.516-29.579-17.13zm-84.159-123.342h64.22v-41.082h-179v41.082h63.906v182.918h50.874z"
|
||||||
|
fill="#fff"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
|
@ -15,22 +15,22 @@
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 text-white shadow-2xs',
|
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 text-white shadow-2xs',
|
||||||
outline:
|
outline:
|
||||||
'bg-background hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 border shadow-2xs',
|
'bg-background hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input dark:hover:bg-input border shadow-2xs',
|
||||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-2xs',
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-2xs',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent',
|
||||||
link: 'text-primary underline-offset-4 hover:underline'
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
icon: 'size-9'
|
icon: 'size-9',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
size: 'default'
|
size: 'default',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
|
|
||||||
6
src/lib/components/ui/sidebar/index.ts
Normal file
6
src/lib/components/ui/sidebar/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Root from './sidebar.svelte';
|
||||||
|
import Sidebar from './sidebar-sidebar.svelte';
|
||||||
|
import Inset from './sidebar-inset.svelte';
|
||||||
|
import Trigger from './sidebar-trigger.svelte';
|
||||||
|
|
||||||
|
export { Root, Sidebar, Inset, Trigger };
|
||||||
10
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
10
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils/utils';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let { class: className, children, ...rest }: HTMLAttributes<HTMLDivElement> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...rest} class={cn('bg-background col-start-2', className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
16
src/lib/components/ui/sidebar/sidebar-sidebar.svelte
Normal file
16
src/lib/components/ui/sidebar/sidebar-sidebar.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils/utils';
|
||||||
|
import { box } from 'svelte-toolbelt';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { useSidebarSidebar } from './sidebar.svelte.js';
|
||||||
|
|
||||||
|
let { class: className, children, ...rest }: HTMLAttributes<HTMLDivElement> = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebarSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if sidebar.root.showSidebar}
|
||||||
|
<div {...rest} class={cn('bg-sidebar col-start-1 h-screen w-[--sidebar-width]', className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
29
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
29
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button, type ButtonElementProps } from '$lib/components/ui/button';
|
||||||
|
import { cn } from '$lib/utils/utils.js';
|
||||||
|
import { useSidebarTrigger } from './sidebar.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = 'ghost',
|
||||||
|
size = 'icon',
|
||||||
|
onclick,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: ButtonElementProps = $props();
|
||||||
|
|
||||||
|
const trigger = useSidebarTrigger();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
{variant}
|
||||||
|
{size}
|
||||||
|
class={cn('', className)}
|
||||||
|
onclick={(e: Parameters<NonNullable<ButtonElementProps['onclick']>>[0]) => {
|
||||||
|
onclick?.(e);
|
||||||
|
trigger.toggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</Button>
|
||||||
18
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
18
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils/utils';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { useSidebar } from './sidebar.svelte.js';
|
||||||
|
|
||||||
|
let { children, ...rest }: HTMLAttributes<HTMLDivElement> = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
class={cn('[--sidebar-width:0px] md:grid md:grid-cols-[var(--sidebar-width)_1fr]', {
|
||||||
|
'[--sidebar-width:250px]': sidebar.showSidebar
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
50
src/lib/components/ui/sidebar/sidebar.svelte.ts
Normal file
50
src/lib/components/ui/sidebar/sidebar.svelte.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
|
||||||
|
import { Context } from 'runed';
|
||||||
|
|
||||||
|
export class SidebarRootState {
|
||||||
|
open = $state(true);
|
||||||
|
openMobile = $state(false);
|
||||||
|
isMobile = new IsMobile();
|
||||||
|
|
||||||
|
showSidebar = $derived(this.isMobile.current ? this.openMobile : this.open);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.toggle = this.toggle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isMobile.current) {
|
||||||
|
this.openMobile = !this.openMobile;
|
||||||
|
} else {
|
||||||
|
this.open = !this.open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SidebarTriggerState {
|
||||||
|
constructor(readonly root: SidebarRootState) {
|
||||||
|
this.toggle = this.toggle.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.root.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SidebarSidebarState {
|
||||||
|
constructor(readonly root: SidebarRootState) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ctx = new Context<SidebarRootState>('sidebar-root-context');
|
||||||
|
|
||||||
|
export function useSidebar() {
|
||||||
|
return ctx.set(new SidebarRootState());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebarTrigger() {
|
||||||
|
return new SidebarTriggerState(ctx.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebarSidebar() {
|
||||||
|
return new SidebarSidebarState(ctx.get());
|
||||||
|
}
|
||||||
9
src/lib/hooks/is-mobile.svelte.ts
Normal file
9
src/lib/hooks/is-mobile.svelte.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { MediaQuery } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export class IsMobile extends MediaQuery {
|
||||||
|
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
|
||||||
|
super(`max-width: ${breakpoint - 1}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
|
||||||
import { authClient } from '$lib/backend/auth/client';
|
|
||||||
|
|
||||||
async function signInGithub() {
|
|
||||||
try {
|
|
||||||
console.log('signInGithub');
|
|
||||||
const data = await authClient.signIn.social({
|
|
||||||
provider: 'github'
|
|
||||||
});
|
|
||||||
console.log('signInGithub data', data);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('signInGithub error', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Button onClickPromise={signInGithub}>Sign In Github</Button>
|
|
||||||
11
src/routes/account/+layout.server.ts
Normal file
11
src/routes/account/+layout.server.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { redirectToLogin } from '$lib/backend/auth/redirect.js';
|
||||||
|
|
||||||
|
export async function load({ locals, url }) {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session) redirectToLogin(url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
session
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { active } from '$lib/actions/active.svelte';
|
import { active } from '$lib/actions/active.svelte';
|
||||||
|
import { authClient } from '$lib/backend/auth/client.js';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import { LightSwitch } from '$lib/components/ui/light-switch';
|
import { LightSwitch } from '$lib/components/ui/light-switch';
|
||||||
import { ArrowLeftIcon } from '@lucide/svelte';
|
import { ArrowLeftIcon } from '@lucide/svelte';
|
||||||
import { Avatar } from 'melt/components';
|
import { Avatar } from 'melt/components';
|
||||||
|
|
||||||
let { children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
const navigation: { title: string; href: string }[] = [
|
const navigation: { title: string; href: string }[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -25,6 +27,12 @@
|
||||||
href: '/account/api-keys'
|
href: '/account/api-keys'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
async function signOut() {
|
||||||
|
await authClient.signOut();
|
||||||
|
|
||||||
|
await goto('/login');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto max-w-[1200px] space-y-8 pt-6 pb-24">
|
<div class="container mx-auto max-w-[1200px] space-y-8 pt-6 pb-24">
|
||||||
|
|
@ -35,27 +43,27 @@
|
||||||
</a>
|
</a>
|
||||||
<div class="flex place-items-center gap-2">
|
<div class="flex place-items-center gap-2">
|
||||||
<LightSwitch variant="ghost" />
|
<LightSwitch variant="ghost" />
|
||||||
<Button variant="ghost">Sign out</Button>
|
<Button variant="ghost" onClickPromise={signOut}>Sign out</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="px-4 md:grid md:grid-cols-[280px_1fr]">
|
<div class="px-4 md:grid md:grid-cols-[280px_1fr]">
|
||||||
<div class="hidden md:col-start-1 md:block">
|
<div class="hidden md:col-start-1 md:block">
|
||||||
<div class="flex flex-col place-items-center gap-2">
|
<div class="flex flex-col place-items-center gap-2">
|
||||||
<Avatar src="https://github.com/shadcn.png">
|
<Avatar src={data.session.user.image}>
|
||||||
{#snippet children(avatar)}
|
{#snippet children(avatar)}
|
||||||
<img {...avatar.image} alt="Avatar" class="size-40 rounded-full" />
|
<img {...avatar.image} alt="Your avatar" class="size-40 rounded-full" />
|
||||||
<span {...avatar.fallback}>JD</span>
|
<span {...avatar.fallback}>{data.session.user.name}</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h1 class="text-center text-2xl font-bold">John Doe</h1>
|
<h1 class="text-center text-2xl font-bold">{data.session.user.name}</h1>
|
||||||
<span class="text-muted-foreground text-center text-sm">m@example.com</span>
|
<span class="text-muted-foreground text-center text-sm">{data.session.user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-8 pl-12 md:col-start-2">
|
<div class="space-y-8 pl-12 md:col-start-2">
|
||||||
<div
|
<div
|
||||||
class="bg-card text-muted-foreground gap-2 flex w-fit place-items-center rounded-lg p-1 text-sm"
|
class="bg-card text-muted-foreground flex w-fit place-items-center gap-2 rounded-lg p-1 text-sm"
|
||||||
>
|
>
|
||||||
{#each navigation as tab (tab)}
|
{#each navigation as tab (tab)}
|
||||||
<a
|
<a
|
||||||
|
|
|
||||||
44
src/routes/chat/+layout.svelte
Normal file
44
src/routes/chat/+layout.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||||
|
import { PanelLeftIcon } from '@lucide/svelte';
|
||||||
|
import { Avatar } from 'melt/components';
|
||||||
|
|
||||||
|
let { data, children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Root>
|
||||||
|
<Sidebar.Sidebar class="flex flex-col p-2">
|
||||||
|
<div class="flex place-items-center justify-center py-2">
|
||||||
|
<span class="text-center text-lg font-bold">Thom Chat</span>
|
||||||
|
</div>
|
||||||
|
<Button href="/chat" class="w-full">New Chat</Button>
|
||||||
|
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||||
|
<!-- chats -->
|
||||||
|
</div>
|
||||||
|
<div class="py-2">
|
||||||
|
{#if data.session !== null}
|
||||||
|
<Button href="/account/api-keys" variant="ghost" class="h-auto w-full justify-start">
|
||||||
|
<Avatar src={data.session.user.image}>
|
||||||
|
{#snippet children(avatar)}
|
||||||
|
<img {...avatar.image} alt="Your avatar" class="size-10 rounded-full" />
|
||||||
|
<span {...avatar.fallback}>{data.session?.user.name}</span>
|
||||||
|
{/snippet}
|
||||||
|
</Avatar>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm">{data.session?.user.name}</span>
|
||||||
|
<span class="text-muted-foreground text-xs">{data.session?.user.email}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button href="/login" class="w-full">Login</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Sidebar.Sidebar>
|
||||||
|
<Sidebar.Inset>
|
||||||
|
<Sidebar.Trigger class="fixed top-3 left-2">
|
||||||
|
<PanelLeftIcon />
|
||||||
|
</Sidebar.Trigger>
|
||||||
|
{@render children()}
|
||||||
|
</Sidebar.Inset>
|
||||||
|
</Sidebar.Root>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Icons from '$lib/components/icons';
|
||||||
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
|
import { SendIcon } from '@lucide/svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex size-full place-items-center justify-center">
|
||||||
|
<div class="flex w-full max-w-lg flex-col place-items-center gap-1">
|
||||||
|
<form class="relative w-full h-18">
|
||||||
|
<textarea
|
||||||
|
class="border-input bg-background h-full ring-ring ring-offset-background w-full resize-none rounded-lg border p-2 text-sm ring-offset-2 outline-none focus-visible:ring-2"
|
||||||
|
placeholder="Ask me anything..."
|
||||||
|
></textarea>
|
||||||
|
<Button type="submit" size="icon" class="absolute bottom-1 right-1 size-8">
|
||||||
|
<SendIcon />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<div class="flex w-full place-items-center justify-between gap-2">
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
Crafted by <Icons.Svelte class="inline size-3" /> wizards.
|
||||||
|
</span>
|
||||||
|
<a href="https://github.com/TGlide/thom-chat" class="text-muted-foreground text-xs">
|
||||||
|
Source on <Icons.GitHub class="ml-0.5 inline size-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
9
src/routes/login/+page.server.ts
Normal file
9
src/routes/login/+page.server.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { redirectToAuthorized } from '$lib/backend/auth/redirect';
|
||||||
|
|
||||||
|
export async function load({ locals, url }) {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (session) redirectToAuthorized(url);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
16
src/routes/login/+page.svelte
Normal file
16
src/routes/login/+page.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as Icons from '$lib/components/icons';
|
||||||
|
import { authClient } from '$lib/backend/auth/client.js';
|
||||||
|
|
||||||
|
async function signIn() {
|
||||||
|
await authClient.signIn.social({ provider: 'github', callbackURL: '/chat' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-svh flex-col place-items-center justify-center gap-4">
|
||||||
|
<h1 class="text-2xl font-bold">Sign in to thom.chat</h1>
|
||||||
|
<Button variant="outline" onClickPromise={signIn}>
|
||||||
|
<Icons.GitHub /> Continue with GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Reference in a new issue