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
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> </form>
<Dialog.Footer>
<Button type="submit">Create</Button>
</Dialog.Footer>
</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,36 +1,34 @@
<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 };
</script> </script>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay /> <Dialog.Overlay />
<DialogPrimitive.Content <DialogPrimitive.Content
{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" />
> <span class="sr-only">Close</span>
<Cross2 class="h-4 w-4" /> </DialogPrimitive.Close>
<span class="sr-only">Close</span> </DialogPrimitive.Content>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal> </Dialog.Portal>

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)} <slot />
{...$$restProps}
>
<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)} <slot />
{...$$restProps}
>
<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,21 +1,20 @@
<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 };
</script> </script>
<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,9 +1,9 @@
<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>
<DialogPrimitive.Portal {...$$restProps}> <DialogPrimitive.Portal {...$$restProps}>
<slot /> <slot />
</DialogPrimitive.Portal> </DialogPrimitive.Portal>

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)} <slot />
{...$$restProps}
>
<slot />
</DialogPrimitive.Title> </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 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;
const Close = DialogPrimitive.Close; const Close = DialogPrimitive.Close;
export { export {
Root, Root,
Title, Title,
Portal, Portal,
Footer, Footer,
Header, Header,
Trigger, Trigger,
Overlay, Overlay,
Content, Content,
Description, Description,
Close, Close,
// //
Root as Dialog, Root as Dialog,
Title as DialogTitle, Title as DialogTitle,
Portal as DialogPortal, Portal as DialogPortal,
Footer as DialogFooter, Footer as DialogFooter,
Header as DialogHeader, Header as DialogHeader,
Trigger as DialogTrigger, Trigger as DialogTrigger,
Overlay as DialogOverlay, Overlay as DialogOverlay,
Content as DialogContent, Content as DialogContent,
Description as DialogDescription, Description as DialogDescription,
Close as DialogClose, 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 // 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;