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", "prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"sveltekit-superforms": "^2.23.1",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwind-variants": "^0.3.1", "tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.20.0",
"vite": "^6.0.0" "vite": "^6.0.0",
"zod": "^3.24.1"
}, },
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",

View File

@ -2,6 +2,13 @@
import { Button, buttonVariants } from '$lib/components/ui/button/index.js'; import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js'; import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Input } from '$lib/components/ui/input/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> </script>
<Dialog.Root> <Dialog.Root>
@ -10,11 +17,19 @@
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Create Channel</Dialog.Title> <Dialog.Title>Create Channel</Dialog.Title>
</Dialog.Header> </Dialog.Header>
<form class="grid gap-4 py-4"> <form class="grid gap-4 py-4" use:enhance method="POST" action="/">
<Input id="channelName" name="channelName" placeholder="Channel Name" type="text" /> <Input
</form> 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> <Dialog.Footer>
<Button type="submit">Create</Button> <Button type="submit">Create</Button>
</Dialog.Footer> </Dialog.Footer>
</form>
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>

View File

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

View File

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from 'bits-ui';
import Cross2 from "svelte-radix/Cross2.svelte"; import Cross2 from 'svelte-radix/Cross2.svelte';
import * as Dialog from "./index.js"; import * as Dialog from './index.js';
import { cn, flyAndScale } from "$lib/utils.js"; import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = DialogPrimitive.ContentProps; type $$Props = DialogPrimitive.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"] = { export let transitionConfig: $$Props['transitionConfig'] = {
duration: 200, duration: 200,
}; };
export { className as class }; export { className as class };
@ -20,15 +20,13 @@
{transition} {transition}
{transitionConfig} {transitionConfig}
class={cn( 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", '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 className,
)} )}
{...$$restProps} {...$$restProps}>
>
<slot /> <slot />
<DialogPrimitive.Close <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" 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" /> <Cross2 class="h-4 w-4" />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>

View File

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

View File

@ -1,16 +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<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 <div class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...$$restProps}>
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot /> <slot />
</div> </div>

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<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("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}> <div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}>
<slot /> <slot />
</div> </div>

View File

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

View File

@ -1,5 +1,5 @@
<script lang="ts"> <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> </script>

View File

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

View File

@ -1,12 +1,12 @@
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from 'bits-ui';
import Title from "./dialog-title.svelte"; import Title from './dialog-title.svelte';
import Portal from "./dialog-portal.svelte"; import Portal from './dialog-portal.svelte';
import Footer from "./dialog-footer.svelte"; import Footer from './dialog-footer.svelte';
import Header from "./dialog-header.svelte"; import Header from './dialog-header.svelte';
import Overlay from "./dialog-overlay.svelte"; import Overlay from './dialog-overlay.svelte';
import Content from "./dialog-content.svelte"; import Content from './dialog-content.svelte';
import Description from "./dialog-description.svelte"; import Description from './dialog-description.svelte';
const Root = DialogPrimitive.Root; const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger; const Trigger = DialogPrimitive.Trigger;

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 // Get Channels method
async getChannels(): Promise<cassandra.types.Row[] | undefined> { async getChannels(): Promise<cassandra.types.Row[] | undefined> {
try { 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 { 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() { export async function load() {
const form = await superValidate(zod(newChannelSchema));
const rows = await db.getChannels(); const rows = await db.getChannels();
const channels: string[] = rows const channels: string[] = rows
? rows.map((value) => { ? rows.map((value) => {
@ -10,5 +14,6 @@ export async function load() {
return { return {
channels, channels,
form,
}; };
} }

View File

@ -4,10 +4,9 @@
import MainLayout from '$lib/components/mainLayout.svelte'; import MainLayout from '$lib/components/mainLayout.svelte';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
let { data, children }: LayoutProps = $props(); let { data, children }: LayoutProps = $props();
const channels = data.channels;
</script> </script>
<ModeWatcher /> <ModeWatcher />
<MainLayout {channels}> <MainLayout data={data.form} channels={data.channels}>
{@render children()} {@render children()}
</MainLayout> </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 { export function load(): void {
redirect(308, '/channel/general'); 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;