feat: Add functionality to channel creation dialog

This commit is contained in:
April Hall 2025-02-07 11:13:55 -05:00
parent 5cef539040
commit e8f634f759
No known key found for this signature in database
GPG Key ID: A49AC35CB186266C
19 changed files with 209 additions and 125 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -30,12 +30,14 @@
"prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"sveltekit-superforms": "^2.23.1",
"tailwind-merge": "^3.0.1",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.0"
"vite": "^6.0.0",
"zod": "^3.24.1"
},
"dependencies": {
"@sveltejs/adapter-node": "^5.2.12",

View File

@ -2,6 +2,13 @@
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import type { SuperValidated, Infer } from 'sveltekit-superforms';
import { superForm } from 'sveltekit-superforms';
import type { NewChannelSchema } from '$lib/types/schema';
import { Label } from '$lib/components/ui/label/index';
let { data }: { data: SuperValidated<Infer<NewChannelSchema>> } = $props();
const { form, errors, constraints, enhance } = superForm(data);
</script>
<Dialog.Root>
@ -10,11 +17,19 @@
<Dialog.Header>
<Dialog.Title>Create Channel</Dialog.Title>
</Dialog.Header>
<form class="grid gap-4 py-4">
<Input id="channelName" name="channelName" placeholder="Channel Name" type="text" />
<form class="grid gap-4 py-4" use:enhance method="POST" action="/">
<Input
id="channelName"
name="channelName"
placeholder="Channel Name"
type="text"
bind:value={$form.channelName}
aria-invalid={$errors.channelName ? 'true' : undefined}
{...$constraints.channelName} />
{#if $errors.channelName}<Label for="channelName" class="text-red-500 m-0 p-0">{$errors.channelName}</Label>{/if}
<Dialog.Footer>
<Button type="submit">Create</Button>
</Dialog.Footer>
</form>
<Dialog.Footer>
<Button type="submit">Create</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@ -1,17 +1,26 @@
<script lang="ts">
import MessagesSquare from 'lucide-svelte/icons/messages-square';
import type { SuperValidated } from 'sveltekit-superforms';
import ChannelDialog from './channelDialog.svelte';
import { Button } from '$lib/components/ui/button/index';
import ModeSwitcher from './modeSwitcher.svelte';
import Channel from './channel.svelte';
import type { Snippet } from 'svelte';
interface Props {
data: SuperValidated<
{
channelName: string;
},
any,
{
channelName: string;
}
>;
channels: string[];
children: Snippet;
}
const { channels, children }: Props = $props();
const { data, channels, children }: Props = $props();
</script>
<div class="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
@ -32,7 +41,7 @@
</nav>
</div>
<div class="mt-auto p-4">
<ChannelDialog />
<ChannelDialog {data} />
</div>
</div>
</div>

View File

@ -1,36 +1,34 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import Cross2 from "svelte-radix/Cross2.svelte";
import * as Dialog from "./index.js";
import { cn, flyAndScale } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui';
import Cross2 from 'svelte-radix/Cross2.svelte';
import * as Dialog from './index.js';
import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = DialogPrimitive.ContentProps;
type $$Props = DialogPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 200,
};
export { className as class };
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props['transitionConfig'] = {
duration: 200,
};
export { className as class };
</script>
<Dialog.Portal>
<Dialog.Overlay />
<DialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
className
)}
{...$$restProps}
>
<slot />
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<Cross2 class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
<Dialog.Overlay />
<DialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
'bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full',
className,
)}
{...$$restProps}>
<slot />
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<Cross2 class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View File

@ -1,16 +1,13 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = DialogPrimitive.DescriptionProps;
type $$Props = DialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<DialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
<DialogPrimitive.Description class={cn('text-muted-foreground text-sm', className)} {...$$restProps}>
<slot />
</DialogPrimitive.Description>

View File

@ -1,16 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>;
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot />
<div class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...$$restProps}>
<slot />
</div>

View File

@ -1,13 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>;
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
<slot />
<div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}>
<slot />
</div>

View File

@ -1,21 +1,20 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui';
import { fade } from 'svelte/transition';
import { cn } from '$lib/utils.js';
type $$Props = DialogPrimitive.OverlayProps;
type $$Props = DialogPrimitive.OverlayProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class };
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = fade;
export let transitionConfig: $$Props['transitionConfig'] = {
duration: 150,
};
export { className as class };
</script>
<DialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
{...$$restProps}
/>
{transition}
{transitionConfig}
class={cn('bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ', className)}
{...$$restProps} />

View File

@ -1,9 +1,9 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { Dialog as DialogPrimitive } from 'bits-ui';
type $$Props = DialogPrimitive.PortalProps;
type $$Props = DialogPrimitive.PortalProps;
</script>
<DialogPrimitive.Portal {...$$restProps}>
<slot />
<slot />
</DialogPrimitive.Portal>

View File

@ -1,16 +1,13 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
type $$Props = DialogPrimitive.TitleProps;
type $$Props = DialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<DialogPrimitive.Title
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
<DialogPrimitive.Title class={cn('text-lg font-semibold leading-none tracking-tight', className)} {...$$restProps}>
<slot />
</DialogPrimitive.Title>

View File

@ -1,37 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import { Dialog as DialogPrimitive } from 'bits-ui';
import Title from "./dialog-title.svelte";
import Portal from "./dialog-portal.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Title from './dialog-title.svelte';
import Portal from './dialog-portal.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View File

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

View File

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

View File

@ -62,6 +62,19 @@ class Db {
}
}
async checkChannel(channel: string): Promise<boolean> {
try {
const res = await this.client.execute(`SELECT table_name FROM system_schema.tables WHERE keyspace_name = 'channels' AND table_name = ?`, [
channel.toLowerCase(),
]);
return res.rowLength !== 0;
} catch (e) {
console.log(`Error checking channel existance: ${e as Error}`);
return false;
}
}
// Get Channels method
async getChannels(): Promise<cassandra.types.Row[] | undefined> {
try {

7
src/lib/types/schema.ts Normal file
View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const newChannelSchema = z.object({
channelName: z.string().min(1, 'Channel name is required'),
});
export type NewChannelSchema = typeof newChannelSchema;

View File

@ -1,6 +1,10 @@
import { db } from '$lib/server/db';
import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms';
import { newChannelSchema } from '$lib/types/schema';
export async function load() {
const form = await superValidate(zod(newChannelSchema));
const rows = await db.getChannels();
const channels: string[] = rows
? rows.map((value) => {
@ -10,5 +14,6 @@ export async function load() {
return {
channels,
form,
};
}

View File

@ -4,10 +4,9 @@
import MainLayout from '$lib/components/mainLayout.svelte';
import { ModeWatcher } from 'mode-watcher';
let { data, children }: LayoutProps = $props();
const channels = data.channels;
</script>
<ModeWatcher />
<MainLayout {channels}>
<MainLayout data={data.form} channels={data.channels}>
{@render children()}
</MainLayout>

View File

@ -1,5 +1,29 @@
import { redirect } from '@sveltejs/kit';
import { redirect, fail } from '@sveltejs/kit';
import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate, message } from 'sveltekit-superforms';
import { newChannelSchema } from '$lib/types/schema';
import type { Actions } from './$types';
import { db } from '$lib/server/db';
export function load(): void {
redirect(308, '/channel/general');
}
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod(newChannelSchema));
const channel = form.data.channelName;
if (!form.valid) {
return fail(400, { form });
}
if (await db.checkChannel(channel)) {
return setError(form, 'channelName', 'Channel already exists.');
}
db.createChannel(channel);
return message(form, 'Channel created!');
},
} satisfies Actions;