1
0
This commit is contained in:
2024-05-21 12:44:32 +03:00
parent 170c19594e
commit bc3ff5e6e8
14 changed files with 261 additions and 128 deletions

View File

@@ -14,7 +14,6 @@
height: 100vh;
width: 100vw;
margin: 0;
background-color: black;
}
</style>

View File

@@ -1,14 +1,15 @@
<script lang="ts">
import { Bell } from 'lucide-svelte';
import { Button } from './ui/button';
import { type Notification } from '$lib/types';
import { isErrorResponse, type Notification } from '$lib/types';
import * as Popover from '$lib/components/ui/popover';
import { cn } from '$lib/utils';
import { ScrollArea } from './ui/scroll-area';
import { Separator } from './ui/separator';
import { getAllNotifications } from '$lib/api/notification';
import { appWebsocket } from '$lib/stores/websocket';
export let notifications: Notification[] = [];
export let onSeenNotification: (notification: Notification) => void = () => {};
let className = '';
@@ -32,6 +33,31 @@
}
$: if (open) onNotificationClick();
let notifications: Notification[] = [];
getAllNotifications().then((data) => {
if (!isErrorResponse(data)) {
notifications = data;
}
});
appWebsocket.on_autoDispose('createNotification', (data) => {
const typedNotification = data as Notification;
notifications = [typedNotification, ...notifications];
});
appWebsocket.on_autoDispose('seenNotification', (data) => {
const typedId = data as { id: number };
notifications = notifications.map((notification) => {
if (notification.id === typedId.id) {
notification.seen = true;
}
return notification;
});
});
</script>
<Popover.Root bind:open>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import * as ContextMenu from '$lib/components/ui/context-menu';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { type Secret } from '$lib/types';
import { Trash2 } from 'lucide-svelte';
import { deleteSecret } from '$lib/api/secret';
export let secret: Secret;
let deleteSecretDialogOpen = false;
</script>
<AlertDialog.Root bind:open={deleteSecretDialogOpen}>
<AlertDialog.Trigger></AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Are you absolutely sure?</AlertDialog.Title>
<AlertDialog.Description>
This action <strong>cannot</strong> be undone.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
<AlertDialog.Action on:click={() => deleteSecret(secret.id)}>
Continue
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<ContextMenu.Root closeOnEscape={true}>
<ContextMenu.Trigger>
<slot />
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Label>
<span class="font-bold">{secret.name}</span>
</ContextMenu.Label>
<ContextMenu.Item on:click={() => (deleteSecretDialogOpen = true)}>
<Trash2 />
<span class="ml-2">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>

View File

@@ -11,9 +11,11 @@ function initialTheme() {
export const theme: Writable<'light' | 'dark'> = persisted('theme', initialTheme());
theme.subscribe((theme) => {
export function updateTheme(theme: 'light' | 'dark') {
if (!browser) return;
if (theme === 'light') document.documentElement.classList.remove('dark');
else document.documentElement.classList.add('dark');
});
}
theme.subscribe((value) => updateTheme(value));

View File

@@ -32,7 +32,7 @@
<ScrollArea class="h-full w-full">
<div
contenteditable="true"
class="h-full w-full break-words break-all contain-layout"
class="h-full w-full break-words break-all contain-layout min-h-14 outline-none"
bind:innerText={content}
on:paste={onPaste}
></div>

View File

@@ -11,31 +11,6 @@
import { getAllNotifications, seenNotification } from '$lib/api/notification';
export let createChannelForm: SuperValidated<CreateChannelFormSchema>;
let notifications: Notification[] = [];
getAllNotifications().then((data) => {
if (!isErrorResponse(data)) {
notifications = data;
}
});
appWebsocket.on_autoDispose('createNotification', (data) => {
const typedNotification = data as Notification;
notifications = [typedNotification, ...notifications];
});
appWebsocket.on_autoDispose('seenNotification', (data) => {
const typedId = data as { id: number };
notifications = notifications.map((notification) => {
if (notification.id === typedId.id) {
notification.seen = true;
}
return notification;
});
});
</script>
<div class="mx-4 my-3 flex">
@@ -49,7 +24,6 @@
<div>
<Notifications
{notifications}
onSeenNotification={(notification) => seenNotification(notification.id)}
/>
</div>

View File

@@ -4,10 +4,11 @@
export const addUserToChannelFormSchema = z.object({
name: z
.string()
.min(6, { message: 'Name must be at least 6 characters' })
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, {
message: 'Name can only contain letters, numbers, and underscores'
})
.min(6, 'Name must be at least 6 characters')
.regex(
/^[a-zA-Z_][a-zA-Z0-9_]*$/,
'Name can only contain letters, numbers, and underscores'
)
});
export type AddUserToChannelFormSchema = z.infer<typeof addUserToChannelFormSchema>;
@@ -36,6 +37,4 @@
const { form: formData, enhance } = form;
</script>
<div>
</div>
<div></div>

View File

@@ -4,10 +4,11 @@
export const createChannelFormSchema = z.object({
name: z
.string()
.min(6, { message: 'Name must be at least 6 characters' })
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, {
message: 'Name can only contain letters, numbers, and underscores'
})
.min(6, 'Name must be at least 6 characters')
.regex(
/^[a-zA-Z_][a-zA-Z0-9_]*$/,
'Name can only contain letters, numbers, and underscores'
)
});
export type CreateChannelFormSchema = z.infer<typeof createChannelFormSchema>;

View File

@@ -1,13 +1,15 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import type { Secret } from '$lib/types';
import SecretContext from '$lib/components/secret-context.svelte';
export let secret: Secret;
export let selected: boolean = false;
export let onClick: (secret: Secret) => void = () => {};
</script>
<div>
<SecretContext {secret}>
<div>
<Button
on:click={() => onClick(secret)}
variant={selected ? 'default' : 'outline'}
@@ -15,4 +17,5 @@
>
{secret.name}
</Button>
</div>
</div>
</SecretContext>

View File

@@ -16,9 +16,8 @@
}
</script>
<div class="flex w-full flex-col">
<div class="mt-4 h-full w-full">
<ScrollArea class="flex h-full w-full flex-col ">
<div class="my-2">
<Button
href="/secrets/new"
variant="outline"
@@ -28,11 +27,16 @@
<Plus class="mr-2 h-4 w-4" />
New secret
</Button>
</div>
<div class="mt-4 flex w-full flex-col gap-2">
{#each secrets as secret}
<div class="my-2">
<SecretsListItem {secret} selected={selectedId === secret.id} onClick={select} />
<div class="flex w-full flex-col">
<SecretsListItem
{secret}
selected={selectedId === secret.id}
onClick={select}
/>
</div>
{/each}
</div>
</ScrollArea>
</div>

View File

@@ -6,7 +6,8 @@
content: z.string().min(1, 'Message is required'),
timeoutSeconds: z.number().int().nonnegative(),
recipients: z.array(z.number().int().nonnegative())
});
})
.refine((data) => data.recipients.length > 0, 'At least one recipient is required');
export type SecretFormSchema = z.infer<typeof secretFormSchema>;
</script>
@@ -22,12 +23,16 @@
import { afterUpdate, beforeUpdate, onMount } from 'svelte';
import { Textarea } from '$lib/components/ui/textarea';
import { getFollowedUsers } from '$lib/api/user';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import * as Avatar from '$lib/components/ui/avatar';
import { cn } from '$lib/utils';
export let data: SuperValidated<SecretFormSchema>;
export let defaultSecret: Secret | undefined = undefined;
export let followedUsers: User[] = [];
export let recipients: User[] = [];
export let reset: boolean = false;
export let name: string = 'Create Secret';
const form = superForm(data, {
validators: zodClient(secretFormSchema),
@@ -61,54 +66,105 @@
});
</script>
<form method="post" use:enhance>
<div class="flex h-full w-full items-center justify-center">
<div
class="max-h-[80%] w-[60%] rounded-lg bg-secondary shadow-md shadow-secondary-foreground contain-content dark:shadow-none"
>
<ScrollArea class="h-full">
<div class="p-4">
<div>
{#if name}
<h1 class="text-center text-2xl font-bold">{name}</h1>
{/if}
</div>
<form method="post" use:enhance class="flex flex-col">
<Form.Field name="name" {form}>
<Form.Control let:attrs>
<Form.Label>Name</Form.Label>
<Input {...attrs} type="text" placeholder="Name" bind:value={$formData.name} />
<Input
{...attrs}
type="text"
placeholder="Name"
bind:value={$formData.name}
/>
<Form.FieldErrors />
</Form.Control>
</Form.Field>
<Form.Field name="content" {form}>
<Form.Control let:attrs>
<Form.Label>Message</Form.Label>
<Textarea {...attrs} placeholder="Message" bind:value={$formData.content} />
<Textarea
{...attrs}
placeholder="Message"
bind:value={$formData.content}
/>
<Form.FieldErrors />
</Form.Control>
</Form.Field>
<Form.Field name="timeoutSeconds" {form}>
<Form.Control let:attrs>
<Form.Label>Timeout</Form.Label>
<Input {...attrs} type="number" placeholder="Timeout" bind:value={seconds} />
<Input
{...attrs}
type="number"
placeholder="Timeout"
bind:value={seconds}
/>
<Form.FieldErrors />
</Form.Control>
</Form.Field>
<span class="pb-2 text-sm">Recipients</span>
<Form.Fieldset {form} name="recipients">
<div class="flex flex-col gap-2">
<div class="mb-2 flex flex-row flex-wrap gap-2">
{#each followedUsers as user}
{@const checked = $formData.recipients.includes(user.id)}
<Form.Control let:attrs>
{@const { name, ...rest } = attrs}
<div>
<Checkbox
{...rest}
{checked}
<button
type="button"
class={cn(
'rounded-lg px-2 py-1 hover:bg-background',
checked && 'bg-accent hover:bg-accent'
)}
on:click={() => {
if (checked) {
$formData.recipients = $formData.recipients.filter(
(id) => id !== user.id
);
} else {
$formData.recipients = [...$formData.recipients, user.id];
$formData.recipients = [
...$formData.recipients,
user.id
];
}
}}
></Checkbox>
<input type="checkbox" {name} hidden value={user.id} {checked} />
>
<Form.Control let:attrs>
<div class="flex flex-row items-center gap-2">
<input
type="checkbox"
{...attrs}
hidden
value={user.id}
{checked}
/>
<Avatar.Root class="h-6 w-6">
<Avatar.Image src={user.avatar} />
<Avatar.Fallback>
{user.username[0]}
</Avatar.Fallback>
</Avatar.Root>
<span>{user.username}</span>
</div>
</Form.Control>
</button>
{/each}
<Form.FieldErrors />
</div>
</Form.Fieldset>
<Form.Button>Save</Form.Button>
</form>
<div class="self-center">
<Form.Button type="submit">Save</Form.Button>
</div>
</form>
</div>
</ScrollArea>
</div>
</div>

View File

@@ -1,9 +1,14 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { appWebsocket } from '$lib/stores/websocket';
import type { Secret } from '$lib/types';
import { isErrorResponse, type Secret } from '$lib/types';
import { MoveLeft } from 'lucide-svelte';
import type { LayoutData } from './$types';
import SecretsList from './(components)/secrets-list.svelte';
import ThemeSwitch from '$lib/components/theme-switch.svelte';
import Notifications from '$lib/components/notifications.svelte';
import { getAllNotifications } from '$lib/api/notification';
import { type Notification } from '$lib/types';
export let data: LayoutData;
@@ -28,10 +33,18 @@
});
</script>
<div class="flex h-full w-full flex-row">
<div class="h-full w-[20%] min-w-[200px] max-w-[400px] space-y-4 rounded-lg bg-secondary p-4">
<Button variant="outline" href="/channels">Back to channels</Button>
<div class="flex h-screen flex-row">
<div class="hidden max-w-[370px] flex-col bg-secondary p-4 sm:w-[50%] md:flex">
<div class="flex h-12 items-center justify-between">
<Button variant="outline" href="/channels"><MoveLeft /></Button>
<div>
<Notifications />
<ThemeSwitch />
</div>
</div>
<div class="w-full">
<SecretsList {secrets} />
</div>
</div>
<slot></slot>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { followingUserCache, usersCache } from '$lib/stores/cache';
import { usersCache } from '$lib/stores/cache';
import { appWebsocket } from '$lib/stores/websocket';
import type { Secret } from '$lib/types';
import type { PageData } from './$types';
@@ -61,5 +61,11 @@
</script>
{#key secret}
<SecretForm defaultSecret={secret} data={data.form} {followedUsers} {recipients} />
<SecretForm
name={secret.name}
defaultSecret={secret}
data={data.form}
{followedUsers}
{recipients}
/>
{/key}

View File

@@ -1,11 +1,17 @@
<script>
import '../app.css';
import { theme } from '$lib/stores/theme';
import { theme, updateTheme } from '$lib/stores/theme';
import { Toaster } from '$lib/components/ui/sonner';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
onMount(() => {
updateTheme($theme);
});
</script>
<Toaster theme={$theme} />
<div class="h-full w-full">
<div class="h-screen w-screen">
<slot></slot>
</div>