channel creation / deletion
This commit is contained in:
parent
f1dcd0cfc5
commit
d0509788ff
19 changed files with 577 additions and 28 deletions
|
|
@ -11,6 +11,7 @@
|
||||||
import UsersRound from '@lucide/svelte/icons/users-round';
|
import UsersRound from '@lucide/svelte/icons/users-round';
|
||||||
import CirclePlus from '@lucide/svelte/icons/circle-plus';
|
import CirclePlus from '@lucide/svelte/icons/circle-plus';
|
||||||
import Input from './ui/input/input.svelte';
|
import Input from './ui/input/input.svelte';
|
||||||
|
import * as ContextMenu from '$lib/components/ui/context-menu/index.js';
|
||||||
import Button, { buttonVariants } from './ui/button/button.svelte';
|
import Button, { buttonVariants } from './ui/button/button.svelte';
|
||||||
import User from './extra/User.svelte';
|
import User from './extra/User.svelte';
|
||||||
import type { SessionValidationResult } from '$lib/server/auth';
|
import type { SessionValidationResult } from '$lib/server/auth';
|
||||||
|
|
@ -482,7 +483,10 @@
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<Sidebar.MenuSub>
|
<Sidebar.MenuSub>
|
||||||
{#each overview_data.servers as server (server.id)}
|
{#each overview_data.servers as server (server.id)}
|
||||||
<Sidebar.MenuSubItem>
|
<Dialog.Root>
|
||||||
|
<ContextMenu.Root>
|
||||||
|
<ContextMenu.Trigger
|
||||||
|
><Sidebar.MenuSubItem>
|
||||||
<Sidebar.MenuSubButton
|
<Sidebar.MenuSubButton
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -498,7 +502,67 @@
|
||||||
{server.name}
|
{server.name}
|
||||||
</Sidebar.MenuSubButton>
|
</Sidebar.MenuSubButton>
|
||||||
</Sidebar.MenuSubItem>
|
</Sidebar.MenuSubItem>
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
<ContextMenu.Content>
|
||||||
|
<ContextMenu.Item>
|
||||||
|
<Dialog.Trigger>Create channel</Dialog.Trigger>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
|
|
||||||
|
<Dialog.Content>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="?/createChannel"
|
||||||
|
use:enhance={() => {
|
||||||
|
return async ({ result }) => {
|
||||||
|
if (result.type == 'success') {
|
||||||
|
toast.success('Created channel successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type == 'error' || result.type == 'failure') {
|
||||||
|
toast.error(
|
||||||
|
'Could not create channel: ' +
|
||||||
|
(result.type === 'error' ? result.error : result.data?.error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await invalidateAll();
|
||||||
|
await fill_overview_data(psd);
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Create a channel</Dialog.Title>
|
||||||
|
<Dialog.Description>Add a new channel to this server.</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label for="channelName">Channel name</Label>
|
||||||
|
<Input
|
||||||
|
id="channelName"
|
||||||
|
name="channelName"
|
||||||
|
placeholder="Channel name"
|
||||||
|
required
|
||||||
|
minlength={1}
|
||||||
|
maxlength={32}
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="serverId" value={server.id} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Dialog.Close class={buttonVariants({ variant: 'outline' })}
|
||||||
|
>Cancel</Dialog.Close
|
||||||
|
>
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
{#each server.channels as channel (channel.id)}
|
{#each server.channels as channel (channel.id)}
|
||||||
|
<Dialog.Root>
|
||||||
|
<ContextMenu.Root>
|
||||||
|
<ContextMenu.Trigger>
|
||||||
<a
|
<a
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -510,6 +574,56 @@
|
||||||
>
|
>
|
||||||
{channel.name}
|
{channel.name}
|
||||||
</a>
|
</a>
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
<ContextMenu.Content>
|
||||||
|
<ContextMenu.Item class="text-red-500 underline">
|
||||||
|
<Dialog.Trigger>Delete channel</Dialog.Trigger>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
|
|
||||||
|
<Dialog.Content>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="?/deleteChannel"
|
||||||
|
use:enhance={() => {
|
||||||
|
return async ({ result }) => {
|
||||||
|
if (result.type == 'success') {
|
||||||
|
toast.success('Deleted channel successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type == 'error' || result.type == 'failure') {
|
||||||
|
toast.error(
|
||||||
|
'Could not delete channel: ' +
|
||||||
|
(result.type === 'error' ? result.error : result.data?.error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await invalidateAll();
|
||||||
|
await fill_overview_data(psd);
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input type="hidden" name="serverId" value={server.id} />
|
||||||
|
<input type="hidden" name="channelId" value={channel.id} />
|
||||||
|
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Delete this channel</Dialog.Title>
|
||||||
|
<Dialog.Description
|
||||||
|
>This action is IRREVERSIBLE and will delete all messages in the
|
||||||
|
channel. Are you sure you want to proceed?</Dialog.Description
|
||||||
|
>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Dialog.Close class={buttonVariants({ variant: 'outline' })}
|
||||||
|
>No</Dialog.Close
|
||||||
|
>
|
||||||
|
<Button variant="destructive" type="submit">Yes</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
</Sidebar.MenuSub>
|
</Sidebar.MenuSub>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
data-slot="context-menu-checkbox-item"
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<span
|
||||||
|
class="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
{#if checked}
|
||||||
|
<CheckIcon class="size-4" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.()}
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import ContextMenuPortal from "./context-menu-portal.svelte";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
portalProps,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ContextMenuPrimitive.ContentProps & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPortal {...portalProps}>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--bits-context-menu-content-available-height) min-w-[8rem] origin-(--bits-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</ContextMenuPortal>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
...restProps
|
||||||
|
}: ContextMenuPrimitive.GroupHeadingProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-group-heading"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:ps-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} />
|
||||||
27
src/lib/components/ui/context-menu/context-menu-item.svelte
Normal file
27
src/lib/components/ui/context-menu/context-menu-item.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...restProps
|
||||||
|
}: ContextMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
24
src/lib/components/ui/context-menu/context-menu-label.svelte
Normal file
24
src/lib/components/ui/context-menu/context-menu-label.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="context-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:ps-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Portal {...restProps} />
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(""),
|
||||||
|
...restProps
|
||||||
|
}: ContextMenuPrimitive.RadioGroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="context-menu-radio-group"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-radio-item"
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<span
|
||||||
|
class="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
{#if checked}
|
||||||
|
<CircleIcon class="size-2 fill-current" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.({ checked })}
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ContextMenuPrimitive.SeparatorProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-separator"
|
||||||
|
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="context-menu-shortcut"
|
||||||
|
class={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ContextMenuPrimitive.SubContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-sub-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--bits-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="context-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronRightIcon class="ms-auto" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Sub bind:open {...restProps} />
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Trigger bind:ref data-slot="context-menu-trigger" {...restProps} />
|
||||||
7
src/lib/components/ui/context-menu/context-menu.svelte
Normal file
7
src/lib/components/ui/context-menu/context-menu.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuPrimitive.Root bind:open {...restProps} />
|
||||||
52
src/lib/components/ui/context-menu/index.ts
Normal file
52
src/lib/components/ui/context-menu/index.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import Root from "./context-menu.svelte";
|
||||||
|
import Sub from "./context-menu-sub.svelte";
|
||||||
|
import Portal from "./context-menu-portal.svelte";
|
||||||
|
import Trigger from "./context-menu-trigger.svelte";
|
||||||
|
import Group from "./context-menu-group.svelte";
|
||||||
|
import RadioGroup from "./context-menu-radio-group.svelte";
|
||||||
|
import Item from "./context-menu-item.svelte";
|
||||||
|
import GroupHeading from "./context-menu-group-heading.svelte";
|
||||||
|
import Content from "./context-menu-content.svelte";
|
||||||
|
import Shortcut from "./context-menu-shortcut.svelte";
|
||||||
|
import RadioItem from "./context-menu-radio-item.svelte";
|
||||||
|
import Separator from "./context-menu-separator.svelte";
|
||||||
|
import SubContent from "./context-menu-sub-content.svelte";
|
||||||
|
import SubTrigger from "./context-menu-sub-trigger.svelte";
|
||||||
|
import CheckboxItem from "./context-menu-checkbox-item.svelte";
|
||||||
|
import Label from "./context-menu-label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Sub,
|
||||||
|
Portal,
|
||||||
|
Item,
|
||||||
|
GroupHeading,
|
||||||
|
Label,
|
||||||
|
Group,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
Shortcut,
|
||||||
|
Separator,
|
||||||
|
RadioItem,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
RadioGroup,
|
||||||
|
CheckboxItem,
|
||||||
|
//
|
||||||
|
Root as ContextMenu,
|
||||||
|
Sub as ContextMenuSub,
|
||||||
|
Portal as ContextMenuPortal,
|
||||||
|
Item as ContextMenuItem,
|
||||||
|
GroupHeading as ContextMenuGroupHeading,
|
||||||
|
Group as ContextMenuGroup,
|
||||||
|
Content as ContextMenuContent,
|
||||||
|
Trigger as ContextMenuTrigger,
|
||||||
|
Shortcut as ContextMenuShortcut,
|
||||||
|
RadioItem as ContextMenuRadioItem,
|
||||||
|
Separator as ContextMenuSeparator,
|
||||||
|
RadioGroup as ContextMenuRadioGroup,
|
||||||
|
SubContent as ContextMenuSubContent,
|
||||||
|
SubTrigger as ContextMenuSubTrigger,
|
||||||
|
CheckboxItem as ContextMenuCheckboxItem,
|
||||||
|
Label as ContextMenuLabel,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { fail } from '@sveltejs/kit';
|
import { fail } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import * as table from '$lib/server/db/schema';
|
import * as table from '$lib/server/db/schema';
|
||||||
import { InviteID, ServerID } from '$lib';
|
import { ChannelID, InviteID, ServerID } from '$lib';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { _sendToUser } from '../../api/updates/+server';
|
import { _sendToUser } from '../../api/updates/+server';
|
||||||
import type { Actions } from '../$types';
|
import type { Actions } from '../$types';
|
||||||
|
|
@ -192,6 +192,79 @@ export default {
|
||||||
_sendToUser(locals.user!.id, { type: 'server', status: 'server-created' });
|
_sendToUser(locals.user!.id, { type: 'server', status: 'server-created' });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
createChannel: async ({ request, locals }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const name = data.get('channelName');
|
||||||
|
const serverId = data.get('serverId');
|
||||||
|
|
||||||
|
if (typeof name !== 'string' || name.length < 1 || name.length > 32) {
|
||||||
|
return fail(400, { error: 'Channel name must be between 1-32 characters' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof serverId !== 'string') {
|
||||||
|
return fail(400, { error: 'Server ID incorrect' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = await db.select().from(table.server).where(eq(table.server.id, serverId));
|
||||||
|
if (!server || server.length == 0) {
|
||||||
|
return fail(400, { error: 'Server ID invalid' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO check permissions here (if owner ignore, then check roles, if any role has manageChannels then let create channel)
|
||||||
|
|
||||||
|
const channelId = ChannelID.newV4();
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.insert(table.channel).values({
|
||||||
|
id: channelId,
|
||||||
|
serverId: serverId,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(table.server)
|
||||||
|
.set({
|
||||||
|
channels: (server[0].channels as string[]).concat([channelId])
|
||||||
|
})
|
||||||
|
.where(eq(table.server.id, serverId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
deleteChannel: async ({ request, locals }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const serverId = data.get('serverId');
|
||||||
|
const channelId = data.get('channelId');
|
||||||
|
|
||||||
|
if (typeof serverId !== 'string' || typeof channelId !== 'string') {
|
||||||
|
return fail(400, { error: 'Invalid channel data' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = await db.select().from(table.server).where(eq(table.server.id, serverId));
|
||||||
|
if (!server || server.length == 0) {
|
||||||
|
return fail(400, { error: 'Server ID invalid' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await db.select().from(table.channel).where(eq(table.channel.id, channelId));
|
||||||
|
if (!channel || channel.length == 0) {
|
||||||
|
return fail(400, { error: 'Channel not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO check permissions here (if owner ignore, then check roles, if any role has manageChannels then let delete channel)
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.delete(table.channel).where(eq(table.channel.id, channelId));
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(table.server)
|
||||||
|
.set({
|
||||||
|
channels: (server[0].channels as string[]).filter((id) => id !== channelId)
|
||||||
|
})
|
||||||
|
.where(eq(table.server.id, serverId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
deleteServer: async ({ request, locals }) => {
|
deleteServer: async ({ request, locals }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const serverId = data.get('serverId');
|
const serverId = data.get('serverId');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue