switch to use:enchance for all forms, split overview into its own file

This commit is contained in:
Soph :3 2026-01-16 20:44:19 +02:00
parent 365c43c501
commit f47d113a01
5 changed files with 367 additions and 81 deletions

View file

@ -14,18 +14,25 @@
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';
import { Status, statuses, type OverviewData, type OverviewUser } from '$lib'; import { Status, type OverviewData, type OverviewUser } from '$lib';
import Label from './ui/label/label.svelte'; import Label from './ui/label/label.svelte';
import { toast } from 'svelte-sonner';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { fill_overview_data } from '$lib/state.svelte';
import type { PageServerLoad } from '../../routes/app/$types';
let { let {
currentPage = $bindable<string | null>(), currentPage = $bindable<string | null>(),
subPage = $bindable<string | null>(), subPage = $bindable<string | null>(),
data, data,
psd,
user, user,
...restProps ...restProps
}: { }: {
currentPage: string | null; currentPage: string | null;
subPage: string | null; subPage: string | null;
psd: PageServerLoad;
data: OverviewData; data: OverviewData;
user: SessionValidationResult['user']; user: SessionValidationResult['user'];
} = $props(); } = $props();
@ -65,7 +72,27 @@
</Dialog.Header> </Dialog.Header>
<!-- input to add a new friend --> <!-- input to add a new friend -->
<form method="POST" action="?/addFriend" class="mb-4"> <form
method="POST"
action="?/addFriend"
class="mb-4"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Friend request sent successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not send friend request: ' + (result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<Input name="username" placeholder="username" required class="mb-2" /> <Input name="username" placeholder="username" required class="mb-2" />
<Dialog.Footer> <Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close> <Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
@ -93,7 +120,27 @@
<Card.Description>Request sent</Card.Description> <Card.Description>Request sent</Card.Description>
</Card.Header> </Card.Header>
<Card.Footer> <Card.Footer>
<form method="POST" action="?/cancelFriendRequest"> <form
method="POST"
action="?/cancelFriendRequest"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Cancelled friend request successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not cancel friend request: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="requestId" value={request.id} /> <input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Cancel</Button> <Button type="submit" variant="outline" size="sm">Cancel</Button>
</form> </form>
@ -116,12 +163,52 @@
</Card.Header> </Card.Header>
<Card.Footer class="flex gap-2"> <Card.Footer class="flex gap-2">
<!-- accept friend --> <!-- accept friend -->
<form method="POST" action="?/addFriend"> <form
method="POST"
action="?/addFriend"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Accepted friend request successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not accept friend request: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="userId" value={request.fromUser} /> <input type="hidden" name="userId" value={request.fromUser} />
<Button type="submit" size="sm">Accept</Button> <Button type="submit" size="sm">Accept</Button>
</form> </form>
<!-- decline friend --> <!-- decline friend -->
<form method="POST" action="?/cancelFriendRequest"> <form
method="POST"
action="?/cancelFriendRequest"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Cancelled friend request successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not cancel friend request: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="requestId" value={request.id} /> <input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Decline</Button> <Button type="submit" variant="outline" size="sm">Decline</Button>
</form> </form>
@ -141,7 +228,27 @@
<Card.Description>Currently your friend</Card.Description> <Card.Description>Currently your friend</Card.Description>
</Card.Header> </Card.Header>
<Card.Footer> <Card.Footer>
<form method="POST" action="?/removeFriend"> <form
method="POST"
action="?/removeFriend"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Removed friend successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not remove friend: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="userId" value={friend.id} /> <input type="hidden" name="userId" value={friend.id} />
<Button type="submit" variant="destructive" size="sm"> <Button type="submit" variant="destructive" size="sm">
Remove Friend Remove Friend
@ -164,7 +271,26 @@
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content class="sm:max-w-106.25"> <Dialog.Content class="sm:max-w-106.25">
<form method="POST" action="?/createGroup"> <form
method="POST"
action="?/createGroup"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Created group successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not create group: ' + (result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Create a group</Dialog.Title> <Dialog.Title>Create a group</Dialog.Title>
<Dialog.Description>Add friends into your group!</Dialog.Description> <Dialog.Description>Add friends into your group!</Dialog.Description>
@ -193,7 +319,25 @@
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content class="sm:max-w-106.25"> <Dialog.Content class="sm:max-w-106.25">
<form method="POST" action="?/joinServer"> <form
method="POST"
action="?/joinServer"
use:enhance={() => {
return async ({ result }) => {
console.log(result);
if (result.type == 'success') {
toast.success('Joined server successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error('Could not join server: ' + (result.error || result.data?.error));
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Join a server</Dialog.Title> <Dialog.Title>Join a server</Dialog.Title>
<Dialog.Description>Enter an invite link.</Dialog.Description> <Dialog.Description>Enter an invite link.</Dialog.Description>
@ -217,7 +361,26 @@
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content class="sm:max-w-106.25"> <Dialog.Content class="sm:max-w-106.25">
<form method="POST" action="?/createServer"> <form
method="POST"
action="?/createServer"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Created server successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not create server: ' + (result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Create a server</Dialog.Title> <Dialog.Title>Create a server</Dialog.Title>
<Dialog.Description>Name your new server.</Dialog.Description> <Dialog.Description>Name your new server.</Dialog.Description>
@ -358,7 +521,24 @@
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content> <Dialog.Content>
<form method="POST" action="?/updateProfile"> <form
method="POST"
action="?/updateProfile"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Edited profile successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error('Could not edit profile: ' + (result.error || result.data?.error));
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Edit profile</Dialog.Title> <Dialog.Title>Edit profile</Dialog.Title>
<Dialog.Description>Update how others see you.</Dialog.Description> <Dialog.Description>Update how others see you.</Dialog.Description>

View file

@ -8,7 +8,6 @@
import type { SessionValidationResult } from '$lib/server/auth'; import type { SessionValidationResult } from '$lib/server/auth';
import { import {
GroupID, GroupID,
ServerID,
type OverviewData, type OverviewData,
type OverviewGroup, type OverviewGroup,
type OverviewServer, type OverviewServer,
@ -16,6 +15,11 @@
} from '$lib'; } from '$lib';
import Button from './ui/button/button.svelte'; import Button from './ui/button/button.svelte';
import Input from './ui/input/input.svelte'; import Input from './ui/input/input.svelte';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { fill_overview_data } from '$lib/state.svelte';
import { toast } from 'svelte-sonner';
import type { PageServerLoad } from '../../routes/app/$types';
// Props for the member sidebar. // Props for the member sidebar.
let { let {
@ -23,12 +27,14 @@
members = $bindable<OverviewUser[]>([]), members = $bindable<OverviewUser[]>([]),
user, user,
data, data,
psd,
currentEntity, currentEntity,
currentEntityId = $bindable<string | null>(null) currentEntityId = $bindable<string | null>(null)
}: { }: {
open: boolean; open: boolean;
members: OverviewUser[]; members: OverviewUser[];
data: OverviewData; data: OverviewData;
psd: PageServerLoad;
user: SessionValidationResult['user']; user: SessionValidationResult['user'];
currentEntity: OverviewGroup | OverviewServer; currentEntity: OverviewGroup | OverviewServer;
currentEntityId: string | null; currentEntityId: string | null;
@ -62,12 +68,31 @@
<Tabs.Trigger value="admins">Admin Settings</Tabs.Trigger> <Tabs.Trigger value="admins">Admin Settings</Tabs.Trigger>
{/if} {/if}
</Tabs.List> </Tabs.List>
{#if ServerID.is(currentEntityId)} {#if GroupID.is(currentEntityId)}
<h1>not done yet for later</h1>
{:else if GroupID.is(currentEntityId)}
{#if user.id == currentEntity.ownerId} {#if user.id == currentEntity.ownerId}
<Tabs.Content value="admins"> <Tabs.Content value="admins">
<form method="POST" action="?/configureGroup" class="space-y-4 p-2"> <form
method="POST"
action="?/configureGroup"
class="space-y-4 p-2"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Configured group succesfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not configure group successfully: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="groupId" value={currentEntityId} /> <input type="hidden" name="groupId" value={currentEntityId} />
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -111,6 +136,23 @@
method="POST" method="POST"
action="?/deleteGroup" action="?/deleteGroup"
class="border-t border-red-500/30 pt-4" class="border-t border-red-500/30 pt-4"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Group deleted successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not delete group successfully: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
> >
<input type="hidden" name="groupId" value={currentEntityId} /> <input type="hidden" name="groupId" value={currentEntityId} />
@ -129,7 +171,28 @@
)} )}
{#if addableMembers.length !== 0} {#if addableMembers.length !== 0}
<form method="POST" action="?/addMembers" class="space-y-4"> <form
method="POST"
action="?/addMembers"
class="space-y-4"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Users added successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not add users successfully: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="groupId" value={currentEntityId} /> <input type="hidden" name="groupId" value={currentEntityId} />
<h1>Add members</h1> <h1>Add members</h1>
@ -148,7 +211,28 @@
{/if} {/if}
{#if (currentEntity as OverviewGroup).permissions.changeTitle || user.id == currentEntity.ownerId} {#if (currentEntity as OverviewGroup).permissions.changeTitle || user.id == currentEntity.ownerId}
<form method="POST" action="?/changeTitle" class="space-y-4"> <form
method="POST"
action="?/changeTitle"
class="space-y-4"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Title changed successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not change title successfully: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="groupId" value={currentEntityId} /> <input type="hidden" name="groupId" value={currentEntityId} />
<h1>Change title</h1> <h1>Change title</h1>
@ -161,7 +245,28 @@
{/if} {/if}
{#if (currentEntity as OverviewGroup).permissions.removeMembers || user.id == currentEntity.ownerId} {#if (currentEntity as OverviewGroup).permissions.removeMembers || user.id == currentEntity.ownerId}
<form method="POST" action="?/removeMembers" class="space-y-4"> <form
method="POST"
action="?/removeMembers"
class="space-y-4"
use:enhance={() => {
return async ({ result }) => {
if (result.type == 'success') {
toast.success('Users removed successfully');
}
if (result.type == 'error' || result.type == 'failure') {
toast.error(
'Could not remove users successfully: ' +
(result.error || result.data?.error)
);
}
await invalidateAll();
await fill_overview_data(psd);
};
}}
>
<input type="hidden" name="groupId" value={currentEntityId} /> <input type="hidden" name="groupId" value={currentEntityId} />
<h1>Remove members</h1> <h1>Remove members</h1>

53
src/lib/state.svelte.ts Normal file
View file

@ -0,0 +1,53 @@
import { GroupID, ServerID, statuses, UserID, type Channel, type OverviewData } from '$lib';
import type { PageServerData } from '../routes/app/$types';
export const overview_data: OverviewData = $state({
friends: [],
groups: [],
servers: []
});
export async function fill_overview_data(data: PageServerData) {
overview_data.servers = data.user.servers.map((z) => {
return {
id: ServerID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
channels: z.channels as Channel[],
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
});
overview_data.groups = data.user.groups.map((z) => {
return {
id: GroupID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
members: z.members,
permissions: z.permissions,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
});
overview_data.friends = await Promise.all(
data.user.friends.map(async (friend) => {
const res = await fetch(`/api/status/${friend.id}`);
if (!res.ok) {
throw new Error(`Failed to fetch status for ${friend.id}`);
}
const status = await res.json();
statuses.set(friend.id, {
status: status.status,
statusMessage: status.statusMessage
});
return {
id: UserID.parse(friend.id),
username: friend.username,
dmId: friend.dmId,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username
};
})
);
}

View file

@ -35,6 +35,7 @@
import MemberSidebar from '$lib/components/member-sidebar.svelte'; import MemberSidebar from '$lib/components/member-sidebar.svelte';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { fill_overview_data, overview_data } from '$lib/state.svelte';
let { form, data }: { form: ActionData; data: PageServerData } = $props(); let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null); let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
@ -55,59 +56,7 @@
let sessionId: string | undefined = $state(); let sessionId: string | undefined = $state();
let previousSubscription: UserId | GroupId | ServerId | null = $state(null); let previousSubscription: UserId | GroupId | ServerId | null = $state(null);
const overview_data: OverviewData = $state({
friends: [],
groups: [],
servers: []
});
async function fill_overview_data() {
overview_data.servers = data.user.servers.map((z) => {
console.log(z);
return {
id: ServerID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
channels: z.channels as Channel[],
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
});
overview_data.groups = data.user.groups.map((z) => {
return {
id: GroupID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
members: z.members,
permissions: z.permissions,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
});
overview_data.friends = await Promise.all(
data.user.friends.map(async (friend) => {
const res = await fetch(`/api/status/${friend.id}`);
if (!res.ok) {
throw new Error(`Failed to fetch status for ${friend.id}`);
}
const status = await res.json();
statuses.set(friend.id, {
status: status.status,
statusMessage: status.statusMessage
});
return {
id: UserID.parse(friend.id),
username: friend.username,
dmId: friend.dmId,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username
};
})
);
}
$effect(() => { $effect(() => {
console.log(currentPageID, currentSubPageID);
if (currentPageID) { if (currentPageID) {
if (UserID.is(currentPageID)) { if (UserID.is(currentPageID)) {
currentPage = overview_data.friends.find((friend) => friend.id === currentPageID); currentPage = overview_data.friends.find((friend) => friend.id === currentPageID);
@ -118,7 +67,6 @@
} }
if (currentSubPageID) { if (currentSubPageID) {
console.log((currentPage as OverviewServer).channels.find((z) => z.id == currentSubPageID));
currentSubPage = (currentPage as OverviewServer).channels.find( currentSubPage = (currentPage as OverviewServer).channels.find(
(z) => z.id == currentSubPageID (z) => z.id == currentSubPageID
) as Channel | null; ) as Channel | null;
@ -199,9 +147,8 @@
await invalidateAll(); await invalidateAll();
} }
await fill_overview_data(); await fill_overview_data(data);
} }
run(); run();
}); });
@ -222,12 +169,12 @@
if (json.type == 'friends') { if (json.type == 'friends') {
toast('Invalidation from friends updates, recieved ' + json.status); toast('Invalidation from friends updates, recieved ' + json.status);
await invalidateAll(); await invalidateAll();
await fill_overview_data(); await fill_overview_data(data);
} }
if (json.type == 'server') { if (json.type == 'server') {
toast('Invalidation from servers updates, recieved ' + json.status); toast('Invalidation from servers updates, recieved ' + json.status);
await invalidateAll(); await invalidateAll();
await fill_overview_data(); await fill_overview_data(data);
} }
if (json.type == 'group') { if (json.type == 'group') {
toast('Invalidation from group updates, recieved ' + json.status); toast('Invalidation from group updates, recieved ' + json.status);
@ -237,14 +184,12 @@
} }
await invalidateAll(); await invalidateAll();
await fill_overview_data(data);
await fill_overview_data();
} }
if (json.type == 'username') { if (json.type == 'username') {
toast('Invalidation from username updates, recieved ' + json.status); toast('Invalidation from username updates, recieved ' + json.status);
await invalidateAll(); await invalidateAll();
await fill_overview_data(data);
await fill_overview_data();
} }
if (json.type == 'connected') { if (json.type == 'connected') {
console.log('SSE connected. We are sessionID ' + json.sessionId); console.log('SSE connected. We are sessionID ' + json.sessionId);
@ -352,6 +297,7 @@
<Sidebar.Provider> <Sidebar.Provider>
<AppSidebar <AppSidebar
psd={data}
bind:subPage={currentSubPageID} bind:subPage={currentSubPageID}
bind:currentPage={currentPageID} bind:currentPage={currentPageID}
user={data.user} user={data.user}
@ -544,7 +490,7 @@
toast.success('Invite created successfully'); toast.success('Invite created successfully');
inviteCode = location.origin + '/invite/' + result.data!.code; inviteCode = location.origin + '/invite/' + result.data!.code;
await invalidateAll(); await invalidateAll();
await fill_overview_data(); await fill_overview_data(data);
} else if (result.type === 'failure') { } else if (result.type === 'failure') {
toast.error('Failed to create invite: ' + result.data?.error); toast.error('Failed to create invite: ' + result.data?.error);
} }
@ -586,7 +532,7 @@
if (response.ok) { if (response.ok) {
toast.success('Invite deleted successfully'); toast.success('Invite deleted successfully');
await invalidateAll(); await invalidateAll();
await fill_overview_data(); await fill_overview_data(data);
} else { } else {
toast.error('Failed to delete invite'); toast.error('Failed to delete invite');
} }
@ -691,6 +637,7 @@
{#if currentPage && currentPageID && overview_data && members && !UserID.is(currentPageID)} {#if currentPage && currentPageID && overview_data && members && !UserID.is(currentPageID)}
<MemberSidebar <MemberSidebar
bind:open={isMembersTabOpen} bind:open={isMembersTabOpen}
psd={data}
user={data.user} user={data.user}
data={overview_data} data={overview_data}
{members} {members}

View file

@ -94,6 +94,7 @@ export default {
await tx.update(table.group).set({ members: remaining }).where(eq(table.group.id, groupId)); await tx.update(table.group).set({ members: remaining }).where(eq(table.group.id, groupId));
for (const id of remaining) { for (const id of remaining) {
if (id == locals.user!.id) continue;
_sendToUser(id, { type: 'group', status: 'someone-was-removed' }); _sendToUser(id, { type: 'group', status: 'someone-was-removed' });
} }