feat: Add functionality to channel creation dialog
This commit is contained in:
parent
5cef539040
commit
e8f634f759
@ -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",
|
||||
|
@ -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>
|
||||
<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.Content>
|
||||
</Dialog.Root>
|
||||
|
@ -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>
|
||||
|
@ -1,14 +1,14 @@
|
||||
<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;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = flyAndScale;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let transition: $$Props['transition'] = flyAndScale;
|
||||
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||
duration: 200,
|
||||
};
|
||||
export { className as class };
|
||||
@ -20,15 +20,13 @@
|
||||
{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
|
||||
'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}
|
||||
>
|
||||
{...$$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"
|
||||
>
|
||||
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>
|
||||
|
@ -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;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<DialogPrimitive.Description class={cn('text-muted-foreground text-sm', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</DialogPrimitive.Description>
|
||||
|
@ -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>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
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}
|
||||
>
|
||||
<div class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -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>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
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}>
|
||||
<div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<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;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let transition: $$Props["transition"] = fade;
|
||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let transition: $$Props['transition'] = fade;
|
||||
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||
duration: 150,
|
||||
};
|
||||
export { className as class };
|
||||
@ -16,6 +16,5 @@
|
||||
<DialogPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
||||
{...$$restProps}
|
||||
/>
|
||||
class={cn('bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ', className)}
|
||||
{...$$restProps} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
type $$Props = DialogPrimitive.PortalProps;
|
||||
</script>
|
||||
|
@ -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;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<DialogPrimitive.Title class={cn('text-lg font-semibold leading-none tracking-tight', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</DialogPrimitive.Title>
|
||||
|
@ -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 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;
|
||||
|
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Root from './label.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
15
src/lib/components/ui/label/label.svelte
Normal file
15
src/lib/components/ui/label/label.svelte
Normal 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>
|
@ -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
7
src/lib/types/schema.ts
Normal 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;
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user