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

@@ -3,10 +3,7 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": [ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",

View File

@@ -4,67 +4,48 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 253 44% 98%;
--foreground: 222.2 84% 4.9%; --foreground: 253 58% 0%;
--muted: 253 7% 87%;
--muted: 210 40% 96.1%; --muted-foreground: 253 13% 37%;
--muted-foreground: 215.4 16.3% 46.9%; --popover: 253 44% 98%;
--popover-foreground: 253 58% 0%;
--popover: 0 0% 100%; --card: 253 44% 97%;
--popover-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 0%;
--border: 220 13% 91%;
--card: 0 0% 100%; --input: 220 13% 91%;
--card-foreground: 222.2 84% 4.9%; --primary: 253 91% 58%;
--primary-foreground: 253 91% 98%;
--border: 214.3 31.8% 91.4%; --secondary: 253 5% 89%;
--input: 214.3 31.8% 91.4%; --secondary-foreground: 253 5% 29%;
--accent: 253 12% 82%;
--primary: 222.2 47.4% 11.2%; --accent-foreground: 253 12% 22%;
--primary-foreground: 210 40% 98%; --destructive: 339.2 90.36% 51.18%;
--destructive-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; --ring: 253 91% 58%;
--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; --radius: 0.5rem;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 253 43% 3%;
--foreground: 210 40% 98%; --foreground: 253 31% 98%;
--muted: 253 7% 13%;
--muted: 217.2 32.6% 17.5%; --muted-foreground: 253 13% 63%;
--muted-foreground: 215 20.2% 65.1%; --popover: 253 43% 3%;
--popover-foreground: 253 31% 98%;
--popover: 222.2 84% 4.9%; --card: 253 43% 4%;
--popover-foreground: 210 40% 98%; --card-foreground: 253 31% 99%;
--border: 215 27.9% 16.9%;
--card: 222.2 84% 4.9%; --input: 215 27.9% 16.9%;
--card-foreground: 210 40% 98%; --primary: 253 91% 58%;
--primary-foreground: 253 91% 98%;
--border: 217.2 32.6% 17.5%; --secondary: 253 7% 9%;
--input: 217.2 32.6% 17.5%; --secondary-foreground: 253 7% 69%;
--accent: 253 13% 14%;
--primary: 210 40% 98%; --accent-foreground: 253 13% 74%;
--primary-foreground: 222.2 47.4% 11.2%; --destructive: 339.2 90.36% 51.18%;
--destructive-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%; --ring: 253 91% 58%;
--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);
} }
} }

View File

@@ -1,10 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height" /> <meta name="viewport" content="width=device-width, initial-scale=1, height=device-height" />
@@ -21,8 +18,7 @@
} }
</style> </style>
<body data-sveltekit-preload-data="hover" class="dark"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

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

View File

@@ -1,20 +1,31 @@
import { messagesCache, channelsCache } from '$lib/stores/cache';
import type { Channel, Message } from '../types'; import type { Channel, Message } from '../types';
import { apiRequest } from './utils'; import { apiRequest } from './utils';
export async function getAllChannels() { 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) { 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) { 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) { 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) { 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) { 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 type { Message } from '../types';
import { apiRequest } from './utils'; import { apiRequest } from './utils';
export async function getMessageById(messageId: number) { 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) { 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 type { Token, User } from '../types';
import { apiRequest } from './utils'; import { apiRequest } from './utils';
export async function getByToken(token: string | undefined | null) { 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) { 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) { 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) { export async function loginUser(username: string, password: string) {

View File

@@ -1,14 +1,19 @@
import { API_URL } from "$lib/constants"; import { API_URL } from '$lib/constants';
import { getUserToken } from "$lib/stores/user"; import { getUserToken } from '$lib/stores/user';
import type { ErrorResponse } from "$lib/types"; import type { ErrorResponse } from '$lib/types';
import type { AxiosRequestConfig } from "axios"; import type { AxiosRequestConfig } from 'axios';
import axios from "axios"; import axios from 'axios';
export async function apiRequest<T>(
path: string,
export async function apiRequest<T>(path: string, method: 'get' | 'post' | 'put' | 'delete', options?: AxiosRequestConfig & { token?: string }): Promise<T | ErrorResponse> { method: 'get' | 'post' | 'put' | 'delete',
options?: AxiosRequestConfig & { token?: string },
cacheCallback?: (data: T) => void
): Promise<T | ErrorResponse> {
const url = API_URL + path; const url = API_URL + path;
console.log(`[API] ${method.toUpperCase()} ${url}`);
const token = options?.token || getUserToken(); const token = options?.token || getUserToken();
options = { options = {
@@ -16,16 +21,18 @@ export async function apiRequest<T>(path: string, method: 'get' | 'post' | 'put'
method, method,
headers: { headers: {
...options?.headers, ...options?.headers,
'Authorization': `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
validateStatus: () => true, validateStatus: () => true,
...options, ...options
} };
const response = await axios.request(options); const response = await axios.request(options);
if (response.status === 200) if (response.status === 200) {
return response.data as T; const data = response.data as T;
else if (cacheCallback) cacheCallback(data);
return response.data as ErrorResponse;
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,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui"; import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = AvatarPrimitive.FallbackProps; type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)} class={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,34 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from 'tailwind-variants';
import type { Button as ButtonPrimitive } from "bits-ui"; import type { Button as ButtonPrimitive } from 'bits-ui';
import Root from "./button.svelte"; import Root from './button.svelte';
const buttonVariants = tv({ 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", 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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: 'hover:bg-accent hover:text-accent-foreground',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline'
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10'
}, }
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default'
}, }
}); });
type Variant = VariantProps<typeof buttonVariants>["variant"]; type Variant = VariantProps<typeof buttonVariants>['variant'];
type Size = VariantProps<typeof buttonVariants>["size"]; type Size = VariantProps<typeof buttonVariants>['size'];
type Props = ButtonPrimitive.Props & { type Props = ButtonPrimitive.Props & {
variant?: Variant; variant?: Variant;
@@ -45,5 +45,5 @@ export {
Root as Button, Root as Button,
type Props as ButtonProps, type Props as ButtonProps,
type Events as ButtonEvents, type Events as ButtonEvents,
buttonVariants, buttonVariants
}; };

View File

@@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Check from "lucide-svelte/icons/check"; import Check from 'lucide-svelte/icons/check';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.CheckboxItemProps; type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents; type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let checked: $$Props["checked"] = undefined; export let checked: $$Props['checked'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
bind:checked bind:checked
class={cn( 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", '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 className
)} )}
{...$$restProps} {...$$restProps}

View File

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

View File

@@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.ItemProps & { type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean; inset?: boolean;
}; };
type $$Events = DropdownMenuPrimitive.ItemEvents; type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let inset: $$Props["inset"] = undefined; export let inset: $$Props['inset'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
class={cn( 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", '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", inset && 'pl-8',
className className
)} )}
{...$$restProps} {...$$restProps}

View File

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

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <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> </script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value> <DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Circle from "lucide-svelte/icons/circle"; import Circle from 'lucide-svelte/icons/circle';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.RadioItemProps; type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents; type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let value: $$Props["value"]; export let value: $$Props['value'];
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
class={cn( 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", '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 className
)} )}
{value} {value}

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRight from "lucide-svelte/icons/chevron-right"; import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.SubTriggerProps & { type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean; inset?: boolean;
}; };
type $$Events = DropdownMenuPrimitive.SubTriggerEvents; type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let inset: $$Props["inset"] = undefined; export let inset: $$Props['inset'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
class={cn( 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", '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", inset && 'pl-8',
className className
)} )}
{...$$restProps} {...$$restProps}

View File

@@ -1,14 +1,14 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Item from "./dropdown-menu-item.svelte"; import Item from './dropdown-menu-item.svelte';
import Label from "./dropdown-menu-label.svelte"; import Label from './dropdown-menu-label.svelte';
import Content from "./dropdown-menu-content.svelte"; import Content from './dropdown-menu-content.svelte';
import Shortcut from "./dropdown-menu-shortcut.svelte"; import Shortcut from './dropdown-menu-shortcut.svelte';
import RadioItem from "./dropdown-menu-radio-item.svelte"; import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from "./dropdown-menu-separator.svelte"; import Separator from './dropdown-menu-separator.svelte';
import RadioGroup from "./dropdown-menu-radio-group.svelte"; import RadioGroup from './dropdown-menu-radio-group.svelte';
import SubContent from "./dropdown-menu-sub-content.svelte"; import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
const Sub = DropdownMenuPrimitive.Sub; const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root; const Root = DropdownMenuPrimitive.Root;
@@ -44,5 +44,5 @@ export {
RadioGroup as DropdownMenuRadioGroup, RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent, SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger, SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem, CheckboxItem as DropdownMenuCheckboxItem
}; };

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <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 $$Props = Button.Props;
type $$Events = Button.Events; type $$Events = Button.Events;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import Root from "./input.svelte"; import Root from './input.svelte';
export type FormInputEvent<T extends Event = Event> = T & { export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement; currentTarget: EventTarget & HTMLInputElement;
@@ -25,5 +25,5 @@ export type InputEvents = {
export { export {
Root, Root,
// //
Root as Input, Root as Input
}; };

View File

@@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements"; import type { HTMLInputAttributes } from 'svelte/elements';
import type { InputEvents } from "./index.js"; import type { InputEvents } from './index.js';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLInputAttributes; type $$Props = HTMLInputAttributes;
type $$Events = InputEvents; type $$Events = InputEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let value: $$Props["value"] = undefined; export let value: $$Props['value'] = undefined;
export { className as class }; export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305 // Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x. // Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined; export let readonly: $$Props['readonly'] = undefined;
</script> </script>
<input <input
class={cn( 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", '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 className
)} )}
bind:value bind:value

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </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,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner"; import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
import { mode } from "mode-watcher"; import { mode } from 'mode-watcher';
type $$Props = SonnerProps; type $$Props = SonnerProps;
</script> </script>
@@ -10,11 +10,11 @@
class="toaster group" class="toaster group"
toastOptions={{ toastOptions={{
classes: { classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", 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", description: 'group-[.toast]:text-muted-foreground',
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
}, }
}} }}
{...$$restProps} {...$$restProps}
/> />

View File

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

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui"; import { Switch as SwitchPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = SwitchPrimitive.Props; type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events; type $$Events = SwitchPrimitive.Events;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let checked: $$Props["checked"] = undefined; export let checked: $$Props['checked'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<SwitchPrimitive.Root <SwitchPrimitive.Root
bind:checked bind:checked
class={cn( 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", '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 className
)} )}
{...$$restProps} {...$$restProps}
@@ -22,7 +22,7 @@
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
class={cn( 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" '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> </SwitchPrimitive.Root>

View File

@@ -1,4 +1,4 @@
import Root from "./textarea.svelte"; import Root from './textarea.svelte';
type FormTextareaEvent<T extends Event = Event> = T & { type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement; currentTarget: EventTarget & HTMLTextAreaElement;
@@ -24,5 +24,5 @@ export {
// //
Root as Textarea, Root as Textarea,
type TextareaEvents, type TextareaEvents,
type FormTextareaEvent, type FormTextareaEvent
}; };

View File

@@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements"; import type { HTMLTextareaAttributes } from 'svelte/elements';
import type { TextareaEvents } from "./index.js"; import type { TextareaEvents } from './index.js';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLTextareaAttributes; type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents; type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let value: $$Props["value"] = undefined; export let value: $$Props['value'] = undefined;
export { className as class }; export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305 // Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x. // Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined; export let readonly: $$Props['readonly'] = undefined;
</script> </script>
<textarea <textarea
class={cn( 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", '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 className
)} )}
bind:value bind:value

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,21 +1,15 @@
import { derived, get } from "svelte/store"; import { derived, get } from 'svelte/store';
import { isErrorResponse, type User } from "../types"; import { isErrorResponse, type User } from '../types';
import { getByToken } from "$lib/api/user"; import { getByToken } from '$lib/api/user';
import { persisted } from "svelte-persisted-store"; import { persisted } from 'svelte-persisted-store';
export const token = persisted<string | null>('token', null); 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) => { export const user = derived<typeof token, User | null>(token, ($token, set) => {
getByToken($token).then((response) => { getByToken($token).then((response) => {
if (isErrorResponse(response)) if (isErrorResponse(response)) set(null);
set(null); else set(response);
else });
set(response);
})
}); });
export function getUserToken(): string | null { export function getUserToken(): string | null {

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 = { export type ErrorResponse = {
error: string; error: string;
} };
export function isErrorResponse(data: unknown): data is ErrorResponse { export function isErrorResponse(data: unknown): data is ErrorResponse {
return ((data as ErrorResponse).error !== undefined); return (data as ErrorResponse).error !== undefined;
} }
export type Token = { export type Token = {
@@ -11,7 +11,7 @@ export type Token = {
userId: number; userId: number;
createdAt: string; createdAt: string;
expiresAt: string; expiresAt: string;
} };
export type User = { export type User = {
id: number; id: number;

View File

@@ -1,7 +1,7 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
import { cubicOut } from "svelte/easing"; import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from "svelte/transition"; import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -19,7 +19,7 @@ export const flyAndScale = (
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => { ): TransitionConfig => {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = ( const scaleConversion = (
valueA: number, valueA: number,
@@ -35,13 +35,11 @@ export const flyAndScale = (
return valueB; return valueB;
}; };
const styleToString = ( const styleToString = (style: Record<string, number | string | undefined>): string => {
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => { return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str; if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`; return str + `${key}:${style[key]};`;
}, ""); }, '');
}; };
return { return {

View File

@@ -1,2 +1,4 @@
import type { ParamMatcher } from '@sveltejs/kit'; 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 { getByToken } from '$lib/api/user';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
export const prerender = false; // export const prerender = false;
export const ssr = false; // export const ssr = false;
export const load = (async ({ cookies }) => { export const load = (async ({ cookies }) => {
const token = cookies.get('token'); const token = cookies.get('token');
if (!token) if (!token) return redirect(302, '/logout');
return redirect(302, '/logout');
const user = await getByToken(token) const user = await getByToken(token);
if (isErrorResponse(user)) if (isErrorResponse(user)) return redirect(302, '/logout');
return redirect(302, '/logout');
console.log(`User: ${JSON.stringify(user)}`)
return { token, user }; return { token, user };
}) satisfies LayoutServerLoad; }) satisfies LayoutServerLoad;

View File

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

View File

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

View File

@@ -7,21 +7,26 @@
let scrollArea: ScrollArea; 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') { export function scroll(anchor: 'top' | 'bottom', behavior: ScrollBehavior = 'smooth') {
if (scrollArea) { scrollArea.scroll(anchor, behavior);
scrollArea.scroll(anchor);
} }
export function getScrollPercent() {
return scrollArea.getScrollPercent();
} }
</script> </script>
<div class="h-full w-full overflow-y-hidden"> <div class="h-full w-full overflow-y-hidden">
<ScrollArea class="h-full w-full" scrollToBottom={true} bind:this={scrollArea}> <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} {#each messages as message}
<Message {message} /> <Message {message} />
{/each} {/each}
</div> </div>
<!-- <div class="h-[6rem] invisible" /> -->
</ScrollArea> </ScrollArea>
</div> </div>

View File

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

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ScrollArea } from '$lib/components/ui/scroll-area'; import { ScrollArea } from '$lib/components/ui/scroll-area';
import { writable } from 'svelte/store';
import { Send } from 'lucide-svelte'; import { Send } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
@@ -15,20 +14,11 @@
const text = event.clipboardData?.getData('text/plain'); const text = event.clipboardData?.getData('text/plain');
if (text && browser) { 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); document.execCommand('insertText', false, text);
} }
} }
function onSendClick() { function onSendClick() {
console.log(content);
if (content) { if (content) {
onSend(content); onSend(content);
content = ''; content = '';
@@ -36,7 +26,9 @@
} }
</script> </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"> <ScrollArea class="h-full w-full">
<div <div
contenteditable="true" contenteditable="true"

View File

@@ -1,36 +1,48 @@
<script lang="ts"> <script lang="ts">
import { getMessageById } from '$lib/api/message';
import * as Avatar from '$lib/components/ui/avatar'; 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'; import { cn } from '$lib/utils';
export let channel: Channel; export let channel: Channel;
let lastMessage: Message | null = null; let lastMessage: Message | null = null;
if (channel.lastMessageId) { let lastMessageAuthor: User | null = null;
getMessageById(channel.lastMessageId).then((message) => {
if (!isErrorResponse(message)) lastMessage = message; $: 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 selected: boolean = false;
export let onClick: () => void = () => {}; export let onClick: () => void = () => {};
$: className = selected ? 'bg-accent' : 'hover:bg-secondary'; $: buttonColor = selected ? 'bg-accent' : 'hover:bg-secondary-foreground';
</script> </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> <div>
<Avatar.Root class="h-12 w-12"> <Avatar.Root class="h-12 w-12">
<Avatar.Image src="/default-avatar.png" /> <Avatar.Image src="/default-avatar.png" />
<Avatar.Fallback>{channel.name[0].toUpperCase()}</Avatar.Fallback> <Avatar.Fallback>{channel.name[0].toUpperCase()}</Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
</div> </div>
<div class="flex flex-col overflow-auto"> <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 text-left text-xl font-bold">
> {channel.name}
<span class="overflow-hidden text-ellipsis text-left text-sm" </span>
>{lastMessage?.content || ''}</span <span class="overflow-hidden text-ellipsis whitespace-nowrap text-left text-sm">
> {#if lastMessage}
{`${lastMessageAuthor?.username}: ${lastMessage.content}`}
{/if}
</span>
</div> </div>
</button> </button>

View File

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

View File

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

View File

@@ -8,14 +8,15 @@
import type { Icon } from 'lucide-svelte'; import type { Icon } from 'lucide-svelte';
import Settings from 'lucide-svelte/icons/settings'; import Settings from 'lucide-svelte/icons/settings';
import LogOut from 'lucide-svelte/icons/log-out'; 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 type { MenuItem } from './(components)/sidebar-header.svelte';
import ChannelList from './(components)/channel-list.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; export let data: LayoutData;
for (const channel of data.channels) addChannelToCache(channel); let channels = data.channels;
let channelList: ChannelList | undefined; let channelList: ChannelList | undefined;
@@ -44,6 +45,51 @@
]; ];
$: channelId = parseInt($page.params.channel_id); $: 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> </script>
<svelte:window on:keyup={onKeyUp} /> <svelte:window on:keyup={onKeyUp} />
@@ -56,11 +102,7 @@
</div> </div>
<div slot="channels"> <div slot="channels">
<ChannelList <ChannelList {channels} defaultSelected={channelId} bind:this={channelList} />
channels={data.channels}
defaultSelected={channelId}
bind:this={channelList}
/>
</div> </div>
</Sidebar> </Sidebar>
</div> </div>

View File

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

View File

@@ -2,21 +2,40 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ChannelArea from '../(components)/(channel)/channel-area.svelte'; import ChannelArea from '../(components)/(channel)/channel-area.svelte';
import { getCachedChannel } from '$lib/stores/cache/channels'; import { channelsCache } from '$lib/stores/cache';
import type { Channel } from '$lib/types'; import type { Channel, Message } from '$lib/types';
import { addMessageToCache } from '$lib/stores/cache/messages'; import { appWebsocket } from '$lib/stores/websocket';
import { onDestroy, onMount } from 'svelte';
export let data: PageData; export let data: PageData;
let channel: Channel | null = null; let channel: Channel | null = null;
$: channelId = parseInt($page.params.channel_id); $: 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) const messages = data.messages;
addMessageToCache(message);
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> </script>
<div class="flex h-screen w-full items-center justify-center"> <div class="flex h-screen w-full items-center justify-center">
<ChannelArea {channel} messages={data.messages} /> <ChannelArea {channel} {messages} bind:this={channelArea} />
</div> </div>

View File

@@ -1,5 +1,5 @@
import { getMessagesByChannelId } from '$lib/api/channel'; 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 { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
@@ -9,15 +9,13 @@ export const ssr = false;
export const load = (async ({ params, parent }) => { export const load = (async ({ params, parent }) => {
await parent(); await parent();
const channelId = parseInt(params.channel_id) const channelId = parseInt(params.channel_id);
const channel = await getCachedChannel(channelId); const channel = await channelsCache.get(channelId);
if (!channel) if (!channel) return redirect(302, '/channels');
return redirect(302, '/channels');
const messages = await getMessagesByChannelId(channel.id, channel.lastMessageId); const messages = await getMessagesByChannelId(channel.id, channel.lastMessageId);
if (isErrorResponse(messages)) if (isErrorResponse(messages)) return redirect(302, '/channels');
return redirect(302, '/channels');
return { messages }; 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 type { PageServerLoad } from './$types';
import { zod } from "sveltekit-superforms/adapters"; import { zod } from 'sveltekit-superforms/adapters';
import { loginFormSchema } from './login-form.svelte'; import { loginFormSchema } from './login-form.svelte';
import { superValidate } from "sveltekit-superforms"; import { superValidate } from 'sveltekit-superforms';
import { type Actions, fail, redirect } from "@sveltejs/kit"; import { type Actions, fail, redirect } from '@sveltejs/kit';
import { loginUser } from '$lib/api/user'; import { loginUser } from '$lib/api/user';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
export const load = (async ({ cookies }) => { export const load = (async ({ cookies }) => {
if (cookies.get('token')) if (cookies.get('token')) throw redirect(302, '/channels');
throw redirect(302, '/channels');
return { form: await superValidate(zod(loginFormSchema)) }; return { form: await superValidate(zod(loginFormSchema)) };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
@@ -18,8 +16,7 @@ export const actions: Actions = {
default: async (event) => { default: async (event) => {
const form = await superValidate(event, zod(loginFormSchema)); const form = await superValidate(event, zod(loginFormSchema));
if (!form.valid) if (!form.valid) return fail(400, { form });
return fail(400, { form });
const response = await loginUser(form.data.username, form.data.password); 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: '/' }); event.cookies.set('token', response.token, { path: '/' });
return { form, token: response }; 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 type { PageServerLoad } from './$types';
import { superValidate } from "sveltekit-superforms"; import { superValidate } from 'sveltekit-superforms';
import { type Actions, fail } from "@sveltejs/kit"; import { type Actions, fail } from '@sveltejs/kit';
import { zod } from "sveltekit-superforms/adapters"; import { zod } from 'sveltekit-superforms/adapters';
import { registerFormSchema } from './register-form.svelte'; import { registerFormSchema } from './register-form.svelte';
import { registerUser } from '$lib/api/user'; import { registerUser } from '$lib/api/user';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
@@ -14,8 +14,7 @@ export const actions: Actions = {
default: async (event) => { default: async (event) => {
const form = await superValidate(event, zod(registerFormSchema)); const form = await superValidate(event, zod(registerFormSchema));
if (!form.valid) if (!form.valid) return fail(400, { form });
return fail(400, { form });
const response = await registerUser(form.data.username, form.data.password); const response = await registerUser(form.data.username, form.data.password);
@@ -26,4 +25,4 @@ export const actions: Actions = {
return { form, success: true }; 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 { Button } from '$lib/components/ui/button';
import * as Form from '$lib/components/ui/form'; import * as Form from '$lib/components/ui/form';
import { superForm, type SuperValidated } from 'sveltekit-superforms'; import { superForm, type SuperValidated } from 'sveltekit-superforms';
import { Label } from '$lib/components/ui/label';
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { goto } from '$app/navigation';
export let data: SuperValidated<RegisterFormSchema>; export let data: SuperValidated<RegisterFormSchema>;
export let loginUrl: string = '/login'; export let loginUrl: string = '/login';

View File

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

View File

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

@@ -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>

View File

@@ -1,64 +1,64 @@
import { fontFamily } from "tailwindcss/defaultTheme"; import { fontFamily } from 'tailwindcss/defaultTheme';
import type { Config } from "tailwindcss"; import type { Config } from 'tailwindcss';
const config: Config = { const config: Config = {
darkMode: ["class"], darkMode: ['class'],
content: ["./src/**/*.{html,js,svelte,ts}"], content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: ["dark"], safelist: ['dark'],
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px" '2xl': '1400px'
} }
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border) / <alpha-value>)", border: 'hsl(var(--border) / <alpha-value>)',
input: "hsl(var(--input) / <alpha-value>)", input: 'hsl(var(--input) / <alpha-value>)',
ring: "hsl(var(--ring) / <alpha-value>)", ring: 'hsl(var(--ring) / <alpha-value>)',
background: "hsl(var(--background) / <alpha-value>)", background: 'hsl(var(--background) / <alpha-value>)',
foreground: "hsl(var(--foreground) / <alpha-value>)", foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: { primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)", DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: "hsl(var(--primary-foreground) / <alpha-value>)" foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)", DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)" foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)", DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)" foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)", DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: "hsl(var(--muted-foreground) / <alpha-value>)" foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
}, },
accent: { accent: {
DEFAULT: "#AD5CD6", DEFAULT: '#AD5CD6',
foreground: "hsl(var(--accent-foreground) / <alpha-value>)" foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)", DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: "hsl(var(--popover-foreground) / <alpha-value>)" foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
}, },
card: { card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)", DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: "hsl(var(--card-foreground) / <alpha-value>)" foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
} }
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)" sm: 'calc(var(--radius) - 4px)'
}, },
fontFamily: { fontFamily: {
sans: [...fontFamily.sans] sans: [...fontFamily.sans]
} }
} }
}, }
}; };
export default config; export default config;