1
0
This commit is contained in:
2024-05-20 12:20:22 +03:00
parent e87b740e29
commit 13b6c19097
102 changed files with 1569 additions and 1346 deletions

View File

@@ -1,78 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: hsl(212.7,26.8%,83.9);
}
:root {
--background: 253 44% 98%;
--foreground: 253 58% 0%;
--muted: 253 7% 87%;
--muted-foreground: 253 13% 37%;
--popover: 253 44% 98%;
--popover-foreground: 253 58% 0%;
--card: 253 44% 97%;
--card-foreground: 0 0% 0%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--primary: 253 91% 58%;
--primary-foreground: 253 91% 98%;
--secondary: 253 5% 89%;
--secondary-foreground: 253 5% 29%;
--accent: 253 12% 82%;
--accent-foreground: 253 12% 22%;
--destructive: 339.2 90.36% 51.18%;
--destructive-foreground: 0 0% 100%;
--ring: 253 91% 58%;
--radius: 0.5rem;
}
.dark {
--background: 253 43% 3%;
--foreground: 253 31% 98%;
--muted: 253 7% 13%;
--muted-foreground: 253 13% 63%;
--popover: 253 43% 3%;
--popover-foreground: 253 31% 98%;
--card: 253 43% 4%;
--card-foreground: 253 31% 99%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--primary: 253 91% 58%;
--primary-foreground: 253 91% 98%;
--secondary: 253 7% 9%;
--secondary-foreground: 253 7% 69%;
--accent: 253 13% 14%;
--accent-foreground: 253 13% 74%;
--destructive: 339.2 90.36% 51.18%;
--destructive-foreground: 0 0% 100%;
--ring: 253 91% 58%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

2
src/app.d.ts vendored
View File

@@ -10,4 +10,4 @@ declare global {
}
}
export { };
export {};

View File

@@ -1,28 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height" />
%sveltekit.head%
</head>
<head>
<style>
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
background-color: black;
}
</style>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height" />
%sveltekit.head%
</head>
<style>
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
background-color: black;
}
</style>
<body data-sveltekit-preload-data="hover" class="dark">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,6 +1,6 @@
import type { Handle } from "@sveltejs/kit";
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
return response;
};
const response = await resolve(event);
return response;
};

View File

@@ -1,20 +1,31 @@
import { messagesCache, channelsCache } from '$lib/stores/cache';
import type { Channel, Message } from '../types';
import { apiRequest } from './utils';
export async function getAllChannels() {
return await apiRequest<Channel[]>('/channel', 'get');
return await apiRequest<Channel[]>('/channel', 'get', undefined, (data) => {
data.forEach((channel) => {
channelsCache.set(channel.id, channel);
});
});
}
export async function getChannelById(channelId: number) {
return await apiRequest<Channel>(`/channel/${channelId}`, 'get');
return await apiRequest<Channel>(`/channel/${channelId}`, 'get', undefined, (data) => {
channelsCache.set(data.id, data);
});
}
export async function createChannel(name: string) {
return await apiRequest<Channel>('/channel', 'post', { data: { name } });
return await apiRequest<Channel>('/channel', 'post', { data: { name } }, (data) => {
channelsCache.set(data.id, data);
});
}
export async function deleteChannel(channelId: number) {
return await apiRequest<Channel>(`/channel/${channelId}`, 'delete');
return await apiRequest<Channel>(`/channel/${channelId}`, 'delete', undefined, (data) => {
channelsCache.remove(data.id);
});
}
export async function addUserToChannel(channelId: number, userId: number) {
@@ -26,5 +37,14 @@ export async function removeUserFromChannel(channelId: number, userId: number) {
}
export async function getMessagesByChannelId(channelId: number, beforeId?: number, limit?: number) {
return await apiRequest<Message[]>(`/channel/${channelId}/message${beforeId || limit ? '?' : ''}${beforeId ? `before=${beforeId}` : ''}${limit ? `&limit=${limit}` : ''}`, 'get');
}
return await apiRequest<Message[]>(
`/channel/${channelId}/message${beforeId || limit ? '?' : ''}${beforeId ? `before=${beforeId}` : ''}${limit ? `&limit=${limit}` : ''}`,
'get',
undefined,
(data) => {
data.forEach((message) => {
messagesCache.set(message.id, message);
});
}
);
}

View File

@@ -1,10 +1,20 @@
import { messagesCache } from '$lib/stores/cache';
import type { Message } from '../types';
import { apiRequest } from './utils';
export async function getMessageById(messageId: number) {
return await apiRequest<Message>(`/message/${messageId}`, 'get');
return await apiRequest<Message>(`/message/${messageId}`, 'get', undefined, (data) => {
messagesCache.set(data.id, data);
});
}
export async function createMessage(channelId: number, content: string) {
return await apiRequest<Message>('/message', 'post', { data: { channelId, content } });
}
return await apiRequest<Message>(
'/message',
'post',
{ data: { channelId, content } },
(data) => {
messagesCache.set(data.id, data);
}
);
}

View File

@@ -1,16 +1,28 @@
import { usersCache } from '$lib/stores/cache';
import type { Token, User } from '../types';
import { apiRequest } from './utils';
export async function getByToken(token: string | undefined | null) {
return await apiRequest<User>('/user/me', 'get', { token: token as string | undefined });
return await apiRequest<User>(
'/user/me',
'get',
{ token: token as string | undefined },
(data) => {
usersCache.set(data.id, data);
}
);
}
export async function getUserById(userId: number) {
return await apiRequest<User>(`/user/${userId}`, 'get');
return await apiRequest<User>(`/user/${userId}`, 'get', undefined, (data) => {
usersCache.set(data.id, data);
});
}
export async function getUserByUsername(username: string) {
return await apiRequest<User>(`/user/username/${username}`, 'get');
return await apiRequest<User>(`/user/username/${username}`, 'get', undefined, (data) => {
usersCache.set(data.id, data);
});
}
export async function loginUser(username: string, password: string) {
@@ -19,4 +31,4 @@ export async function loginUser(username: string, password: string) {
export async function registerUser(username: string, password: string) {
return await apiRequest<Token>('/user/register', 'post', { data: { username, password } });
}
}

View File

@@ -1,14 +1,19 @@
import { API_URL } from "$lib/constants";
import { getUserToken } from "$lib/stores/user";
import type { ErrorResponse } from "$lib/types";
import type { AxiosRequestConfig } from "axios";
import axios from "axios";
import { API_URL } from '$lib/constants';
import { getUserToken } from '$lib/stores/user';
import type { ErrorResponse } from '$lib/types';
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
export async function apiRequest<T>(path: string, method: 'get' | 'post' | 'put' | 'delete', options?: AxiosRequestConfig & { token?: string }): Promise<T | ErrorResponse> {
export async function apiRequest<T>(
path: string,
method: 'get' | 'post' | 'put' | 'delete',
options?: AxiosRequestConfig & { token?: string },
cacheCallback?: (data: T) => void
): Promise<T | ErrorResponse> {
const url = API_URL + path;
console.log(`[API] ${method.toUpperCase()} ${url}`);
const token = options?.token || getUserToken();
options = {
@@ -16,16 +21,18 @@ export async function apiRequest<T>(path: string, method: 'get' | 'post' | 'put'
method,
headers: {
...options?.headers,
'Authorization': `Bearer ${token}`
Authorization: `Bearer ${token}`
},
validateStatus: () => true,
...options,
}
...options
};
const response = await axios.request(options);
if (response.status === 200)
return response.data as T;
else
return response.data as ErrorResponse;
}
if (response.status === 200) {
const data = response.data as T;
if (cacheCallback) cacheCallback(data);
return data;
} else return response.data as ErrorResponse;
}

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Moon, Sun } from 'lucide-svelte';
import { Button } from './ui/button';
import { theme } from '$lib/stores/theme';
import { cn } from '$lib/utils';
let className = '';
export { className as class };
function switchTheme() {
$theme = $theme === 'light' ? 'dark' : 'light';
}
</script>
<Button variant="ghost" class={cn('h-10 w-10 p-0', className)} on:click={() => switchTheme()}>
<Sun class="hidden dark:block" />
<Moon class="block dark:hidden" />
</Button>

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = AvatarPrimitive.FallbackProps;
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Fallback
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...$$restProps}
class={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
{...$$restProps}
>
<slot />
<slot />
</AvatarPrimitive.Fallback>

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = AvatarPrimitive.ImageProps;
type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined;
export let src: $$Props["src"] = undefined;
export let alt: $$Props["alt"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let src: $$Props['src'] = undefined;
export let alt: $$Props['alt'] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Image
{src}
{alt}
class={cn("aspect-square h-full w-full", className)}
{...$$restProps}
{src}
{alt}
class={cn('aspect-square h-full w-full', className)}
{...$$restProps}
/>

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = AvatarPrimitive.Props;
type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined;
export let delayMs: $$Props["delayMs"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let delayMs: $$Props['delayMs'] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Root
{delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...$$restProps}
{delayMs}
class={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...$$restProps}
>
<slot />
<slot />
</AvatarPrimitive.Root>

View File

@@ -1,13 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
import Root from './avatar.svelte';
import Image from './avatar-image.svelte';
import Fallback from './avatar-fallback.svelte';
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback
};

View File

@@ -1,25 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
import { Button as ButtonPrimitive } from 'bits-ui';
import { type Events, type Props, buttonVariants } from './index.js';
import { cn } from '$lib/utils.js';
type $$Props = Props;
type $$Events = Events;
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
let className: $$Props['class'] = undefined;
export let variant: $$Props['variant'] = 'default';
export let size: $$Props['size'] = 'default';
export let builders: $$Props['builders'] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
<slot />
</ButtonPrimitive.Root>

View File

@@ -1,49 +1,49 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
import { type VariantProps, tv } from 'tailwind-variants';
import type { Button as ButtonPrimitive } from 'bits-ui';
import Root from './button.svelte';
const buttonVariants = tv({
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
base: 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Variant = VariantProps<typeof buttonVariants>['variant'];
type Size = VariantProps<typeof buttonVariants>['size'];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants
};

View File

@@ -1,35 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Check from 'lucide-svelte/icons/check';
import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let checked: $$Props['checked'] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"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-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
bind:checked
class={cn(
'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-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>

View File

@@ -1,27 +1,27 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let sideOffset: $$Props['sideOffset'] = 4;
export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props['transitionConfig'] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
{transition}
{transitionConfig}
{sideOffset}
class={cn(
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none',
className
)}
{...$$restProps}
on:keydown
>
<slot />
<slot />
</DropdownMenuPrimitive.Content>

View File

@@ -1,31 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let inset: $$Props['inset'] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
class={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
<slot />
</DropdownMenuPrimitive.Item>

View File

@@ -1,19 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let inset: $$Props['inset'] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...$$restProps}
>
<slot />
<slot />
</DropdownMenuPrimitive.Label>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
export let value: $$Props['value'] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
<slot />
</DropdownMenuPrimitive.RadioGroup>

View File

@@ -1,35 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Circle from 'lucide-svelte/icons/circle';
import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
let className: $$Props['class'] = undefined;
export let value: $$Props['value'];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"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-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
class={cn(
'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-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.SeparatorProps;
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
{...$$restProps}
class={cn('-mx-1 my-1 h-px bg-muted', className)}
{...$$restProps}
/>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>;
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
<slot />
<span class={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...$$restProps}>
<slot />
</span>

View File

@@ -1,30 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props['transitionConfig'] = {
x: -10,
y: 0
};
export { className as class };
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
{transition}
{transitionConfig}
class={cn(
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none',
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
<slot />
</DropdownMenuPrimitive.SubContent>

View File

@@ -1,32 +1,32 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let inset: $$Props['inset'] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
class={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground',
inset && 'pl-8',
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View File

@@ -1,14 +1,14 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Item from './dropdown-menu-item.svelte';
import Label from './dropdown-menu-label.svelte';
import Content from './dropdown-menu-content.svelte';
import Shortcut from './dropdown-menu-shortcut.svelte';
import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from './dropdown-menu-separator.svelte';
import RadioGroup from './dropdown-menu-radio-group.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
@@ -16,33 +16,33 @@ const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem
};

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
import * as Button from '$lib/components/ui/button/index.js';
type $$Props = Button.Props;
type $$Events = Button.Events;
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
<slot />
</Button.Root>

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import * as FormPrimitive from 'formsnap';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Description
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
class={cn('text-sm text-muted-foreground', className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
<slot {descriptionAttrs} />
</FormPrimitive.Description>

View File

@@ -1,25 +1,25 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
<div class={cn('space-y-2', className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>

View File

@@ -1,26 +1,26 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
export let errorClasses: $$Props['class'] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
class={cn('text-sm font-medium text-destructive', className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>

View File

@@ -1,25 +1,25 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
<div class={cn('space-y-2', className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>

View File

@@ -1,30 +1,30 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldsetProps<T, U>;
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn('space-y-2', className)}
>
<slot {constraints} {errors} {tainted} {value} />
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from "formsnap";
import { cn } from "$lib/utils.js";
import { Label } from "$lib/components/ui/label/index.js";
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils.js';
import { Label } from '$lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props;
type $$Props = LabelPrimitive.Props;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
const { labelAttrs } = getFormControl();
</script>
<Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}>
<slot {labelAttrs} />
<Label {...$labelAttrs} class={cn('data-[fs-error]:text-destructive', className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.LegendProps;
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
{...$$restProps}
class={cn('text-sm font-medium leading-none data-[fs-error]:text-destructive', className)}
let:legendAttrs
>
<slot {legendAttrs} />
<slot {legendAttrs} />
</FormPrimitive.Legend>

View File

@@ -1,33 +1,33 @@
import * as FormPrimitive from "formsnap";
import Description from "./form-description.svelte";
import Label from "./form-label.svelte";
import FieldErrors from "./form-field-errors.svelte";
import Field from "./form-field.svelte";
import Fieldset from "./form-fieldset.svelte";
import Legend from "./form-legend.svelte";
import ElementField from "./form-element-field.svelte";
import Button from "./form-button.svelte";
import * as FormPrimitive from 'formsnap';
import Description from './form-description.svelte';
import Label from './form-label.svelte';
import FieldErrors from './form-field-errors.svelte';
import Field from './form-field.svelte';
import Fieldset from './form-fieldset.svelte';
import Legend from './form-legend.svelte';
import ElementField from './form-element-field.svelte';
import Button from './form-button.svelte';
const Control = FormPrimitive.Control;
export {
Field,
Control,
Label,
Button,
FieldErrors,
Description,
Fieldset,
Legend,
ElementField,
//
Field as FormField,
Control as FormControl,
Description as FormDescription,
Label as FormLabel,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton,
Field,
Control,
Label,
Button,
FieldErrors,
Description,
Fieldset,
Legend,
ElementField,
//
Field as FormField,
Control as FormControl,
Description as FormDescription,
Label as FormLabel,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton
};

View File

@@ -1,29 +1,29 @@
import Root from "./input.svelte";
import Root from './input.svelte';
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
Root,
//
Root as Input
};

View File

@@ -1,42 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
import type { HTMLInputAttributes } from 'svelte/elements';
import type { InputEvents } from './index.js';
import { cn } from '$lib/utils.js';
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let value: $$Props['value'] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props['readonly'] = undefined;
</script>
<input
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
class={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -1,7 +1,7 @@
import Root from "./label.svelte";
import Root from './label.svelte';
export {
Root,
//
Root as Label,
Root,
//
Root as Label
};

View File

@@ -1,21 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
class={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
{...$$restProps}
on:mousedown
>
<slot />
<slot />
</LabelPrimitive.Root>

View File

@@ -1,17 +1,17 @@
import { Popover as PopoverPrimitive } from "bits-ui";
import Content from "./popover-content.svelte";
import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose,
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose
};

View File

@@ -1,22 +1,22 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = PopoverPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
type $$Props = PopoverPrimitive.ContentProps;
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props['transitionConfig'] = undefined;
export { className as class };
</script>
<PopoverPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
className
)}
{...$$restProps}
{transition}
{transitionConfig}
class={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
className
)}
{...$$restProps}
>
<slot />
<slot />
</PopoverPrimitive.Content>

View File

@@ -1,10 +1,10 @@
import Scrollbar from "./scroll-area-scrollbar.svelte";
import Root from "./scroll-area.svelte";
import Scrollbar from './scroll-area-scrollbar.svelte';
import Root from './scroll-area.svelte';
export {
Root,
Scrollbar,
//,
Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar,
Root,
Scrollbar,
//,
Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar
};

View File

@@ -1,27 +1,27 @@
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = ScrollAreaPrimitive.ScrollbarProps & {
orientation?: "vertical" | "horizontal";
};
type $$Props = ScrollAreaPrimitive.ScrollbarProps & {
orientation?: 'vertical' | 'horizontal';
};
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "vertical";
export { className as class };
let className: $$Props['class'] = undefined;
export let orientation: $$Props['orientation'] = 'vertical';
export { className as class };
</script>
<ScrollAreaPrimitive.Scrollbar
{orientation}
class={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px",
orientation === "horizontal" && "h-2.5 w-full border-t border-t-transparent p-px",
className
)}
{orientation}
class={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
orientation === 'horizontal' && 'h-2.5 w-full border-t border-t-transparent p-px',
className
)}
>
<slot />
<ScrollAreaPrimitive.Thumb
class={cn("relative rounded-full bg-border", orientation === "vertical" && "flex-1")}
/>
<slot />
<ScrollAreaPrimitive.Thumb
class={cn('relative rounded-full bg-border', orientation === 'vertical' && 'flex-1')}
/>
</ScrollAreaPrimitive.Scrollbar>

View File

@@ -22,32 +22,34 @@
let viewport: HTMLDivElement;
const scrollBottom = (node: HTMLDivElement, top: number) => {
const scroll = () =>
node.scroll({
top,
behavior: 'instant'
});
scroll();
return { update: scroll };
const scrollTo = (node: HTMLDivElement, top: number, behavior: ScrollBehavior) => {
const scrollNode = () => {
node.scrollTo({ top: top, behavior });
node.scrollTop = top;
};
scrollNode();
};
onMount(() => {
if (scrollToBottom) {
scrollBottom(viewport, viewport.scrollHeight);
scrollTo(viewport, viewport.scrollHeight, 'instant');
}
});
export const scroll = (anchor: 'top' | 'bottom') => {
export const scroll = (anchor: 'top' | 'bottom', behavior: ScrollBehavior = 'smooth') => {
if (anchor === 'bottom') {
scrollBottom(viewport, viewport.scrollHeight);
scrollTo(viewport, viewport.scrollHeight, behavior);
}
if (anchor === 'top') {
scrollBottom(viewport, 0);
scrollTo(viewport, 0, behavior);
}
};
export const getScrollPercent = () => {
const { scrollTop, scrollHeight, clientHeight } = viewport;
return scrollTop / (scrollHeight - clientHeight);
};
</script>
<ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}>

View File

@@ -1,7 +1,7 @@
import Root from "./separator.svelte";
import Root from './separator.svelte';
export {
Root,
//
Root as Separator,
Root,
//
Root as Separator
};

View File

@@ -1,22 +1,22 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Separator as SeparatorPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = SeparatorPrimitive.Props;
type $$Props = SeparatorPrimitive.Props;
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "horizontal";
export let decorative: $$Props["decorative"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let orientation: $$Props['orientation'] = 'horizontal';
export let decorative: $$Props['decorative'] = undefined;
export { className as class };
</script>
<SeparatorPrimitive.Root
class={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{orientation}
{decorative}
{...$$restProps}
class={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{orientation}
{decorative}
{...$$restProps}
/>

View File

@@ -1,7 +1,7 @@
import Root from "./skeleton.svelte";
import Root from './skeleton.svelte';
export {
Root,
//
Root as Skeleton,
Root,
//
Root as Skeleton
};

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>;
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div class={cn("animate-pulse rounded-md bg-muted", className)} {...$$restProps}></div>
<div class={cn('animate-pulse rounded-md bg-muted', className)} {...$$restProps}></div>

View File

@@ -1 +1 @@
export { default as Toaster } from "./sonner.svelte";
export { default as Toaster } from './sonner.svelte';

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
import { mode } from 'mode-watcher';
type $$Props = SonnerProps;
type $$Props = SonnerProps;
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...$$restProps}
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
}
}}
{...$$restProps}
/>

View File

@@ -1,7 +1,7 @@
import Root from "./switch.svelte";
import Root from './switch.svelte';
export {
Root,
//
Root as Switch,
Root,
//
Root as Switch
};

View File

@@ -1,28 +1,28 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Switch as SwitchPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let checked: $$Props['checked'] = undefined;
export { className as class };
</script>
<SwitchPrimitive.Root
bind:checked
class={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...$$restProps}
on:click
on:keydown
bind:checked
class={cn(
'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...$$restProps}
on:click
on:keydown
>
<SwitchPrimitive.Thumb
class={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
<SwitchPrimitive.Thumb
class={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>

View File

@@ -1,28 +1,28 @@
import Root from "./textarea.svelte";
import Root from './textarea.svelte';
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent
};

View File

@@ -1,38 +1,38 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaEvents } from "./index.js";
import { cn } from "$lib/utils.js";
import type { HTMLTextareaAttributes } from 'svelte/elements';
import type { TextareaEvents } from './index.js';
import { cn } from '$lib/utils.js';
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export let value: $$Props['value'] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props['readonly'] = undefined;
</script>
<textarea
class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
class={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View File

@@ -1 +1,2 @@
export const API_URL = 'http://localhost:1234';
export const BASE_API_URL = 'localhost:1234';
export const API_URL = `http://${BASE_API_URL}`;

39
src/lib/event.ts Normal file
View File

@@ -0,0 +1,39 @@
export interface Listener<D> {
(event: D): unknown;
}
export interface Disposable {
dispose(): void;
}
export class EventEmitter<E, D> {
private listeners: Map<E, Listener<D>[]> = new Map();
on = (event: E, listener: Listener<D>): Disposable => {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event)?.push(listener);
return {
dispose: () => this.off(event, listener)
};
};
off = (event: E, listener: Listener<D>) => {
if (!this.listeners.has(event)) return;
const listeners = this.listeners.get(event)?.filter((l) => l !== listener);
this.listeners.set(event, listeners || []);
};
emit = (event: E, data: D) => {
if (!this.listeners.has(event)) return;
this.listeners.get(event)?.forEach((listener) => setTimeout(() => listener(data), 0));
};
pipe = (event: E, te: EventEmitter<E, D>): Disposable => {
return this.on(event, (e) => te.emit(event, e));
};
}

View File

@@ -1,41 +0,0 @@
import { getChannelById } from "$lib/api/channel";
import { isErrorResponse, type Channel } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const channelsCache: Writable<Map<number, Channel>> = writable(new Map<number, Channel>());
const runningCaches = new Set();
export function addChannelToCache(channel: Channel) {
channelsCache.update((channels) => channels.set(channel.id, channel));
}
export async function getCachedChannel(channelId: number): Promise<Channel | null> {
const cached = get(channelsCache).get(channelId);
if (cached)
return cached;
if (runningCaches.has(channelId))
return await new Promise((resolve) => {
channelsCache.subscribe((channels) => {
const channel = channels.get(channelId);
if (channel) {
runningCaches.delete(channelId);
resolve(channel);
}
});
});
runningCaches.add(channelId);
const response = await getChannelById(channelId);
if (isErrorResponse(response))
return null;
const channel = response as Channel;
addChannelToCache(channel);
return channel;
}

23
src/lib/stores/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import { getChannelById } from '$lib/api/channel';
import { getMessageById } from '$lib/api/message';
import { getUserById } from '$lib/api/user';
import { isErrorResponse, type Channel, type Message, type User } from '$lib/types';
import { Cache } from './utils';
export const usersCache: Cache<number, User> = new Cache(async (id) => {
const response = await getUserById(id);
if (isErrorResponse(response)) return null;
return response;
});
export const messagesCache: Cache<number, Message> = new Cache(async (id) => {
const response = await getMessageById(id);
if (isErrorResponse(response)) return null;
return response;
});
export const channelsCache: Cache<number, Channel> = new Cache(async (id) => {
const response = await getChannelById(id);
if (isErrorResponse(response)) return null;
return response;
});

View File

@@ -1,42 +0,0 @@
import { getMessageById } from "$lib/api/message";
import { isErrorResponse, type Message } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const messagesCache: Writable<Map<number, Message>> = writable(new Map<number, Message>());
const runningCaches = new Set();
export function addMessageToCache(message: Message) {
messagesCache.update((messages) => messages.set(message.id, message));
}
export async function getCachedMessage(messageId: number): Promise<Message | null> {
const cached = get(messagesCache).get(messageId);
if (cached)
return cached;
if (runningCaches.has(messageId))
return await new Promise((resolve) => {
messagesCache.subscribe((users) => {
const user = users.get(messageId);
if (user) {
runningCaches.delete(messageId);
resolve(user);
}
});
});
runningCaches.add(messageId);
const response = await getMessageById(messageId);
if (isErrorResponse(response))
return null;
const message = response as Message;
addMessageToCache(message);
return message;
}

View File

@@ -1,47 +0,0 @@
import { getUserById } from "$lib/api/user";
import { isErrorResponse, type User } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const usersCache: Writable<Map<number, User>> = writable(new Map<number, User>());
const runningCaches = new Set();
usersCache.subscribe((users) => {
console.log(`Cached users: ${JSON.stringify([...users.values()])}`);
})
export function addUserToCache(user: User) {
usersCache.update((users) => users.set(user.id, user));
}
export async function getCachedUser(userId: number): Promise<User | null> {
const cachedUser = get(usersCache).get(userId);
if (cachedUser)
return cachedUser;
if (runningCaches.has(userId)) {
return await new Promise((resolve) => {
usersCache.subscribe((users) => {
console.log(`subsribed called`);
const user = users.get(userId);
if (user) {
runningCaches.delete(userId);
resolve(user);
}
});
});
}
runningCaches.add(userId);
const response = await getUserById(userId);
if (isErrorResponse(response))
return null;
const user = response as User;
addUserToCache(user);
return user;
}

57
src/lib/stores/cache/utils.ts vendored Normal file
View File

@@ -0,0 +1,57 @@
import { get, writable, type Writable } from 'svelte/store';
export class Cache<I, T> {
private data: Writable<Map<I, T>> = writable(new Map<I, T>());
private runningCaches: Set<I> = new Set();
private resolver: (data: I) => Promise<T | null>;
constructor(resolver: (data: I) => Promise<T | null>) {
this.resolver = resolver;
}
async get(key: I): Promise<T | null> {
const cached = get(this.data).get(key);
if (cached) {
console.log(`[Cache] Found in cache: `, cached);
return cached;
}
if (this.runningCaches.has(key)) {
return new Promise((resolve) => {
this.data.subscribe((data) => {
const value = data.get(key);
if (value) {
resolve(value);
}
});
});
}
this.runningCaches.add(key);
const data = await this.resolver(key);
this.runningCaches.delete(key);
if (data)
console.log(`[Cache] Added to cache: `, data);
return data;
}
set(key: I, value: T) {
console.log(`[Cache] Added to cache: `, value);
this.data.update((data) => data.set(key, value));
}
remove(key: I) {
console.log(`[Cache] Removed from cache: `, key);
this.data.update((data) => {
data.delete(key);
return data;
});
}
}

19
src/lib/stores/theme.ts Normal file
View File

@@ -0,0 +1,19 @@
import { browser } from '$app/environment';
import { persisted } from 'svelte-persisted-store';
import type { Writable } from 'svelte/store';
function initialTheme() {
if (!browser) return 'light';
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
return 'light';
}
export const theme: Writable<'light' | 'dark'> = persisted('theme', initialTheme());
theme.subscribe((theme) => {
if (!browser) return;
if (theme === 'light') document.documentElement.classList.remove('dark');
else document.documentElement.classList.add('dark');
});

View File

@@ -1,23 +1,17 @@
import { derived, get } from "svelte/store";
import { isErrorResponse, type User } from "../types";
import { getByToken } from "$lib/api/user";
import { persisted } from "svelte-persisted-store";
import { derived, get } from 'svelte/store';
import { isErrorResponse, type User } from '../types';
import { getByToken } from '$lib/api/user';
import { persisted } from 'svelte-persisted-store';
export const token = persisted<string | null>('token', null);
token.subscribe((token) => {
console.log(`updated token: ${JSON.stringify(token)}`);
})
export const user = derived<typeof token, User | null>(token, ($token, set) => {
getByToken($token).then((response) => {
if (isErrorResponse(response))
set(null);
else
set(response);
})
if (isErrorResponse(response)) set(null);
else set(response);
});
});
export function getUserToken(): string | null {
return get(token);
}
}

View File

@@ -0,0 +1,98 @@
import { BASE_API_URL } from '$lib/constants';
import { EventEmitter } from '$lib/event';
import { derived, get } from 'svelte/store';
import { token as tokenStore } from './user';
import type { Channel, Message } from '$lib/types';
import { messagesCache, channelsCache } from './cache';
export type WebSocketMessageType =
| 'createMessage'
| 'updateChannel'
| 'createChannel'
| 'deleteChannel'
| 'connect'
| 'disconnect'
| 'any';
export type WebSocketMessageData =
| Message
| Channel
| { id: number }
| null
| {
type: WebSocketMessageType;
};
export type WebsoketMessage = {
type: WebSocketMessageType;
data: WebSocketMessageData;
};
export const appWebsocket = new EventEmitter<WebSocketMessageType, WebSocketMessageData>();
appWebsocket.on('any', (data) => {
console.log(`[WS] Recieved message: `, data);
});
function updateCache(type: WebSocketMessageType, data: WebSocketMessageData) {
switch (type) {
case 'createMessage':
messagesCache.set((data as Message).id, data as Message);
break;
case 'updateChannel':
channelsCache.set((data as Channel).id, data as Channel);
break;
case 'createChannel':
channelsCache.set((data as Channel).id, data as Channel);
break;
case 'deleteChannel':
channelsCache.remove((data as { id: number }).id);
break;
default:
break;
}
}
const connect = (token: string) => {
const websocket = new WebSocket(`ws://${BASE_API_URL}/ws/${token}`);
websocket.onopen = () => {
appWebsocket.emit('connect', null);
appWebsocket.emit('any', { type: 'connect' });
};
websocket.onmessage = (event) => {
const message: WebsoketMessage = JSON.parse(event.data);
updateCache(message.type, message.data);
appWebsocket.emit(message.type, message.data as WebSocketMessageData);
appWebsocket.emit('any', message);
};
websocket.onclose = () => {
appWebsocket.emit('disconnect', null);
appWebsocket.emit('any', { type: 'disconnect' });
setTimeout(() => {
const token = get(tokenStore);
if (token) connect(token);
}, 500);
};
return websocket;
};
const socket = derived<typeof tokenStore, WebSocket | null>(
tokenStore,
($token, set) => {
if ($token) {
set(connect($token));
} else {
set(null);
}
},
null
);
socket.subscribe((socket) => {
console.log(`[WS] Connected: `, socket);
});

View File

@@ -1,9 +1,9 @@
export type ErrorResponse = {
error: string;
}
};
export function isErrorResponse(data: unknown): data is ErrorResponse {
return ((data as ErrorResponse).error !== undefined);
return (data as ErrorResponse).error !== undefined;
}
export type Token = {
@@ -11,7 +11,7 @@ export type Token = {
userId: number;
createdAt: string;
expiresAt: string;
}
};
export type User = {
id: number;
@@ -33,4 +33,4 @@ export type Channel = {
name: string;
lastMessageId?: number;
createdAt: string;
};
};

View File

@@ -1,62 +1,60 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View File

@@ -1,2 +1,4 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => { return /^\d+$/.test(param); };
export const match: ParamMatcher = (param) => {
return /^\d+$/.test(param);
};

View File

@@ -3,20 +3,16 @@ import type { LayoutServerLoad } from './$types';
import { getByToken } from '$lib/api/user';
import { isErrorResponse } from '$lib/types';
export const prerender = false;
export const ssr = false;
// export const prerender = false;
// export const ssr = false;
export const load = (async ({ cookies }) => {
const token = cookies.get('token');
if (!token)
return redirect(302, '/logout');
if (!token) return redirect(302, '/logout');
const user = await getByToken(token)
if (isErrorResponse(user))
return redirect(302, '/logout');
console.log(`User: ${JSON.stringify(user)}`)
const user = await getByToken(token);
if (isErrorResponse(user)) return redirect(302, '/logout');
return { token, user };
}) satisfies LayoutServerLoad;
}) satisfies LayoutServerLoad;

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import type { LayoutData } from './$types';
import { token, user } from '$lib/stores/user';
import { addUserToCache } from '$lib/stores/cache/users';
import { token } from '$lib/stores/user';
import { usersCache } from '$lib/stores/cache';
import { appWebsocket } from '$lib/stores/websocket';
export let data: LayoutData;
console.log(`loading`);
console.log(data);
token.update(() => data.token);
$token = data.token;
addUserToCache(data.user);
const user = data.user;
usersCache.set(user.id, user);
</script>
<slot />

View File

@@ -1,10 +1,9 @@
<script lang="ts">
import { createMessage } from '$lib/api/message';
import * as Avatar from '$lib/components/ui/avatar';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { isErrorResponse, type Channel, type Message as MessageType } from '$lib/types';
import { type Channel, type Message, type Message as MessageType } from '$lib/types';
import { afterUpdate } from 'svelte';
import MessageArea from './message-area.svelte';
import Message from './message.svelte';
import TextField from './text-field.svelte';
export let channel: Channel | null = null;
@@ -12,34 +11,40 @@
let messageArea: MessageArea;
const sendMessage = (message: string) => {
const sendMessage = (content: string) => {
if (!channel) return;
createMessage(channel.id, message).then((message) => {
if (!isErrorResponse(message)) {
messages = [...messages, message];
messageArea.scroll('bottom');
}
});
createMessage(channel.id, content);
};
export function updateMessages(newMessages: Message[]) {
messages = newMessages;
}
afterUpdate(() => {
if (!messageArea) return;
if (messageArea.getScrollPercent() > 0.95) messageArea.scroll('bottom', 'smooth');
});
</script>
<div class="flex h-screen w-[95%] flex-col contain-strict">
<div class="contents">
<div class="flex items-center space-x-4">
<div class="flex h-screen w-full flex-col items-center contain-strict">
<div class="w-full">
<div class="flex items-center justify-center space-x-4 bg-secondary p-4">
<Avatar.Root class="h-12 w-12">
<Avatar.Image src="/default-avatar.png" />
<Avatar.Fallback>{channel?.name[0].toUpperCase() || ''}</Avatar.Fallback>
</Avatar.Root>
<span class="text-xl font-bold">{channel?.name || ''}</span>
<span class="text-3xl font-bold">{channel?.name || ''}</span>
</div>
</div>
<div class="contents flex-grow">
<MessageArea {messages} bind:this={messageArea} />
</div>
<div class="m-4 mt-1 max-h-[40%] flex-grow">
<TextField onSend={sendMessage} />
<div class="flex h-screen w-[95%] flex-col contain-strict">
<div class="z-[10000] contents flex-grow">
<MessageArea {messages} bind:this={messageArea} />
</div>
<div class="relative bottom-0 left-0 right-0 m-4 mt-1 max-h-[40%] flex-grow">
<TextField onSend={sendMessage} />
</div>
</div>
</div>

View File

@@ -7,21 +7,26 @@
let scrollArea: ScrollArea;
messages = [...messages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
$: messages = [...messages].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
export function scroll(anchor: 'top' | 'bottom') {
if (scrollArea) {
scrollArea.scroll(anchor);
}
export function scroll(anchor: 'top' | 'bottom', behavior: ScrollBehavior = 'smooth') {
scrollArea.scroll(anchor, behavior);
}
export function getScrollPercent() {
return scrollArea.getScrollPercent();
}
</script>
<div class="h-full w-full overflow-y-hidden">
<ScrollArea class="h-full w-full" scrollToBottom={true} bind:this={scrollArea}>
<div class="mx-4 flex flex-col gap-3">
<div class="mx-4 flex flex-col gap-3 pt-4">
{#each messages as message}
<Message {message} />
{/each}
</div>
<!-- <div class="h-[6rem] invisible" /> -->
</ScrollArea>
</div>

View File

@@ -3,15 +3,14 @@
import { user } from '$lib/stores/user';
import { cn } from '$lib/utils';
import * as Avatar from '$lib/components/ui/avatar';
import { getCachedUser } from '$lib/stores/cache/users';
import { usersCache } from '$lib/stores/cache';
import { writable, type Writable } from 'svelte/store';
import { Skeleton } from '$lib/components/ui/skeleton';
export let message: Message;
let sender: Writable<User | null> = writable(null);
getCachedUser(message.authorId).then((user) => ($sender = user));
usersCache.get(message.authorId).then((user) => ($sender = user));
$: username = (isSelf ? $user?.username : $sender?.username) || 'N';
$: isSelf = $user?.id === message.authorId;
@@ -33,7 +32,7 @@
<span class="whitespace-pre-line break-words break-all text-left text-xl font-bold">
{message.content}
</span>
<span class={cn('text-md', timestampPosition)}
<span class={cn('text-sm', timestampPosition)}
>{new Date(message.createdAt).toLocaleString()}
</span>
</div>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { writable } from 'svelte/store';
import { Send } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import { browser } from '$app/environment';
@@ -15,20 +14,11 @@
const text = event.clipboardData?.getData('text/plain');
if (text && browser) {
// if (window.getSelection) {
// var selObj = window.getSelection();
// var selRange = selObj?.getRangeAt(0);
// selRange?.deleteContents();
// selRange?.insertNode(document.createTextNode(text));
// selObj?.collapseToEnd();
// }
document.execCommand('insertText', false, text);
}
}
function onSendClick() {
console.log(content);
if (content) {
onSend(content);
content = '';
@@ -36,7 +26,9 @@
}
</script>
<div class="flex h-full items-center gap-2 rounded-3xl bg-secondary p-2">
<div
class="flex h-full items-center gap-2 rounded-3xl bg-secondary p-2 shadow-md shadow-secondary-foreground dark:shadow-none"
>
<ScrollArea class="h-full w-full">
<div
contenteditable="true"

View File

@@ -1,36 +1,48 @@
<script lang="ts">
import { getMessageById } from '$lib/api/message';
import * as Avatar from '$lib/components/ui/avatar';
import { isErrorResponse, type Channel, type Message } from '$lib/types';
import { usersCache, messagesCache } from '$lib/stores/cache';
import { type Channel, type Message, type User } from '$lib/types';
import { cn } from '$lib/utils';
export let channel: Channel;
let lastMessage: Message | null = null;
if (channel.lastMessageId) {
getMessageById(channel.lastMessageId).then((message) => {
if (!isErrorResponse(message)) lastMessage = message;
});
let lastMessageAuthor: User | null = null;
$: if (lastMessage) {
usersCache.get(lastMessage.authorId).then((user) => (lastMessageAuthor = user));
}
$: if (channel.lastMessageId) {
messagesCache.get(channel.lastMessageId).then((message) => (lastMessage = message));
}
export let selected: boolean = false;
export let onClick: () => void = () => {};
$: className = selected ? 'bg-accent' : 'hover:bg-secondary';
$: buttonColor = selected ? 'bg-accent' : 'hover:bg-secondary-foreground';
</script>
<button on:click={onClick} class={cn('flex w-full space-x-4 rounded-xl p-4', className)}>
<button
on:click={onClick}
class={cn(
'flex w-full space-x-4 rounded-xl p-4 transition-colors contain-inline-size',
buttonColor
)}
>
<div>
<Avatar.Root class="h-12 w-12">
<Avatar.Image src="/default-avatar.png" />
<Avatar.Fallback>{channel.name[0].toUpperCase()}</Avatar.Fallback>
</Avatar.Root>
</div>
<div class="flex flex-col overflow-auto">
<span class="overflow-hidden text-ellipsis text-left text-xl font-bold">{channel.name}</span
>
<span class="overflow-hidden text-ellipsis text-left text-sm"
>{lastMessage?.content || ''}</span
>
<div class="flex flex-col overflow-hidden">
<span class="overflow-hidden text-ellipsis text-left text-xl font-bold">
{channel.name}
</span>
<span class="overflow-hidden text-ellipsis whitespace-nowrap text-left text-sm">
{#if lastMessage}
{`${lastMessageAuthor?.username}: ${lastMessage.content}`}
{/if}
</span>
</div>
</button>

View File

@@ -16,11 +16,13 @@
import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { user } from '$lib/stores/user';
import ThemeSwitch from '$lib/components/theme-switch.svelte';
export let menuItems: MenuItem[] = [];
</script>
<div class="mx-4 my-3 flex w-full">
<div class="mx-4 my-3 flex">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} variant="ghost" class="h-10 w-10 p-0">
@@ -40,4 +42,12 @@
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
<div class="flex-grow pt-1 text-center text-2xl">
{$user?.username}
</div>
<div>
<ThemeSwitch />
</div>
</div>

View File

@@ -1,11 +1,9 @@
<script lang="ts">
import { ScrollArea, ScrollAreaScrollbar } from '$lib/components/ui/scroll-area';
const tags = Array.from({ length: 50 }).map((_, i, a) => `v1.2.0-beta.${a.length - i}`);
import { ScrollArea } from '$lib/components/ui/scroll-area';
</script>
<div>
<div>
<div class="light:shadow-secondary-foreground flex h-full w-full flex-col bg-secondary shadow-lg">
<div class="p-0">
<slot name="header"></slot>
</div>
<div>

View File

@@ -8,14 +8,15 @@
import type { Icon } from 'lucide-svelte';
import Settings from 'lucide-svelte/icons/settings';
import LogOut from 'lucide-svelte/icons/log-out';
import type { ComponentType } from 'svelte';
import { onDestroy, onMount, type ComponentType } from 'svelte';
import type { MenuItem } from './(components)/sidebar-header.svelte';
import ChannelList from './(components)/channel-list.svelte';
import { addChannelToCache } from '$lib/stores/cache/channels';
import { appWebsocket, type WebSocketMessageType } from '$lib/stores/websocket';
import type { Channel } from '$lib/types';
export let data: LayoutData;
for (const channel of data.channels) addChannelToCache(channel);
let channels = data.channels;
let channelList: ChannelList | undefined;
@@ -44,6 +45,51 @@
];
$: channelId = parseInt($page.params.channel_id);
function handleChannelCreated(channel: unknown) {
const typedChannel = channel as Channel;
if (!channelList) return;
channels.push(typedChannel);
}
function handleChannelUpdated(channel: unknown) {
const typedChannel = channel as Channel;
if (!channelList) return;
for (let i = 0; i < channels.length; i++) {
if (channels[i].id == typedChannel.id) {
channels[i] = typedChannel;
break;
}
}
}
function handleChannelDeleted(channel_id: unknown) {
const id = (channel_id as { id: number }).id;
if (!channelList) return;
channels = channels.filter((c) => c.id != id);
}
const handlers = {
createChannel: handleChannelCreated,
updateChannel: handleChannelUpdated,
deleteChannel: handleChannelDeleted
};
onMount(() => {
for (const [key, value] of Object.entries(handlers))
appWebsocket.on(key as WebSocketMessageType, value);
});
onDestroy(() => {
for (const [key, value] of Object.entries(handlers))
appWebsocket.off(key as WebSocketMessageType, value);
});
</script>
<svelte:window on:keyup={onKeyUp} />
@@ -56,11 +102,7 @@
</div>
<div slot="channels">
<ChannelList
channels={data.channels}
defaultSelected={channelId}
bind:this={channelList}
/>
<ChannelList {channels} defaultSelected={channelId} bind:this={channelList} />
</div>
</Sidebar>
</div>

View File

@@ -2,13 +2,14 @@ import { getAllChannels } from '$lib/api/channel';
import type { Channel } from '$lib/types';
import type { LayoutLoad } from './$types';
export const ssr = false;
export const load = (async ({ parent }) => {
await parent();
const response = await getAllChannels();
const channels = response as Channel[]
const channels = response as Channel[];
return { channels };
}) satisfies LayoutLoad;
}) satisfies LayoutLoad;

View File

@@ -2,21 +2,40 @@
import type { PageData } from './$types';
import { page } from '$app/stores';
import ChannelArea from '../(components)/(channel)/channel-area.svelte';
import { getCachedChannel } from '$lib/stores/cache/channels';
import type { Channel } from '$lib/types';
import { addMessageToCache } from '$lib/stores/cache/messages';
import { channelsCache } from '$lib/stores/cache';
import type { Channel, Message } from '$lib/types';
import { appWebsocket } from '$lib/stores/websocket';
import { onDestroy, onMount } from 'svelte';
export let data: PageData;
let channel: Channel | null = null;
$: channelId = parseInt($page.params.channel_id);
$: getCachedChannel(channelId).then((c) => (channel = c));
$: channelsCache.get(channelId).then((c) => (channel = c));
for (const message of data.messages)
addMessageToCache(message);
const messages = data.messages;
let channelArea: ChannelArea;
function handleCreateMessage(message: unknown) {
const typedMessage = message as Message;
if (!channel) return;
if (typedMessage.channelId != channel.id) return;
messages.push(typedMessage);
channelArea?.updateMessages(messages);
}
onMount(() => {
appWebsocket.on('createMessage', handleCreateMessage);
});
onDestroy(() => {
appWebsocket.off('createMessage', handleCreateMessage);
});
</script>
<div class="flex h-screen w-full items-center justify-center">
<ChannelArea {channel} messages={data.messages} />
<ChannelArea {channel} {messages} bind:this={channelArea} />
</div>

View File

@@ -1,5 +1,5 @@
import { getMessagesByChannelId } from '$lib/api/channel';
import { getCachedChannel } from '$lib/stores/cache/channels';
import { channelsCache } from '$lib/stores/cache';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { isErrorResponse } from '$lib/types';
@@ -9,15 +9,13 @@ export const ssr = false;
export const load = (async ({ params, parent }) => {
await parent();
const channelId = parseInt(params.channel_id)
const channel = await getCachedChannel(channelId);
if (!channel)
return redirect(302, '/channels');
const channelId = parseInt(params.channel_id);
const channel = await channelsCache.get(channelId);
if (!channel) return redirect(302, '/channels');
const messages = await getMessagesByChannelId(channel.id, channel.lastMessageId);
if (isErrorResponse(messages))
return redirect(302, '/channels');
if (isErrorResponse(messages)) return redirect(302, '/channels');
return { messages };
}) satisfies PageLoad;
}) satisfies PageLoad;

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import ThemeSwitch from '$lib/components/theme-switch.svelte';
import { Fan } from 'lucide-svelte';
</script>
<div class="flex h-full w-full items-center justify-center">
<ThemeSwitch class="absolute right-4 top-4" />
<div class="flex h-full w-full flex-col items-center justify-center gap-8">
<Fan class="h-32 w-32" />
<slot />
</div>
</div>

View File

@@ -1,15 +1,13 @@
import type { PageServerLoad } from './$types';
import { zod } from "sveltekit-superforms/adapters";
import { zod } from 'sveltekit-superforms/adapters';
import { loginFormSchema } from './login-form.svelte';
import { superValidate } from "sveltekit-superforms";
import { type Actions, fail, redirect } from "@sveltejs/kit";
import { superValidate } from 'sveltekit-superforms';
import { type Actions, fail, redirect } from '@sveltejs/kit';
import { loginUser } from '$lib/api/user';
import { isErrorResponse } from '$lib/types';
export const load = (async ({ cookies }) => {
if (cookies.get('token'))
throw redirect(302, '/channels');
if (cookies.get('token')) throw redirect(302, '/channels');
return { form: await superValidate(zod(loginFormSchema)) };
}) satisfies PageServerLoad;
@@ -18,8 +16,7 @@ export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(loginFormSchema));
if (!form.valid)
return fail(400, { form });
if (!form.valid) return fail(400, { form });
const response = await loginUser(form.data.username, form.data.password);
@@ -31,5 +28,5 @@ export const actions: Actions = {
event.cookies.set('token', response.token, { path: '/' });
return { form, token: response };
},
}
};

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import type { PageData } from './$types';
import LoginForm from './login-form.svelte';
export let data: PageData;
</script>
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Login</h1>
<LoginForm data={data.form}></LoginForm>
</div>

View File

@@ -1,7 +1,7 @@
import type { PageServerLoad } from './$types';
import { superValidate } from "sveltekit-superforms";
import { type Actions, fail } from "@sveltejs/kit";
import { zod } from "sveltekit-superforms/adapters";
import { superValidate } from 'sveltekit-superforms';
import { type Actions, fail } from '@sveltejs/kit';
import { zod } from 'sveltekit-superforms/adapters';
import { registerFormSchema } from './register-form.svelte';
import { registerUser } from '$lib/api/user';
import { isErrorResponse } from '$lib/types';
@@ -14,8 +14,7 @@ export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(registerFormSchema));
if (!form.valid)
return fail(400, { form });
if (!form.valid) return fail(400, { form });
const response = await registerUser(form.data.username, form.data.password);
@@ -26,4 +25,4 @@ export const actions: Actions = {
return { form, success: true };
}
}
};

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import type { PageData } from './$types';
import RegisterForm from './register-form.svelte';
export let data: PageData;
</script>
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Register</h1>
<RegisterForm data={data.form}></RegisterForm>
</div>

View File

@@ -25,9 +25,7 @@
import { Button } from '$lib/components/ui/button';
import * as Form from '$lib/components/ui/form';
import { superForm, type SuperValidated } from 'sveltekit-superforms';
import { Label } from '$lib/components/ui/label';
import { zodClient } from 'sveltekit-superforms/adapters';
import { goto } from '$app/navigation';
export let data: SuperValidated<RegisterFormSchema>;
export let loginUrl: string = '/login';

View File

@@ -1,5 +1,7 @@
<script>
import '../app.css';
import { theme } from '$lib/stores/theme';
</script>
<div class="h-full w-full">

View File

@@ -2,5 +2,5 @@ import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async () => {
redirect(302, '/login')
}) satisfies PageServerLoad;
redirect(302, '/login');
}) satisfies PageServerLoad;

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import { Fan } from 'lucide-svelte';
import type { PageData } from './$types';
import LoginForm from './login-form.svelte';
export let data: PageData;
</script>
<div class="flex h-full w-full flex-col items-center justify-center gap-8">
<Fan class="h-32 w-32" />
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Login</h1>
<LoginForm data={data.form}></LoginForm>
</div>
</div>

View File

@@ -4,4 +4,4 @@ export const load = (async ({ cookies }) => {
cookies.delete('token', { path: '/' });
return {};
}) satisfies PageServerLoad;
}) satisfies PageServerLoad;

View File

@@ -5,7 +5,7 @@
if (browser) {
$token = null;
goto('/login');
}
</script>

View File

@@ -1,14 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { PageData } from './$types';
import RegisterForm from './register-form.svelte';
export let data: PageData;
</script>
<div class="flex h-full w-full items-center justify-center">
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Register</h1>
<RegisterForm data={data.form}></RegisterForm>
</div>
</div>