add members sidebar + endpoint

This commit is contained in:
Soph :3 2026-01-07 02:24:27 +02:00
parent d333cbff04
commit 6f3ceb6838
7 changed files with 195 additions and 103 deletions

View file

@ -0,0 +1,93 @@
<script lang="ts">
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus';
import UserRoundMinus from '@lucide/svelte/icons/user-round-minus';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
import { onMount } from 'svelte';
import User from './extra/User.svelte';
import type { SessionValidationResult } from '$lib/server/auth';
import { ServerID, type UserWithStatus } from '$lib';
// Props for the member sidebar.
let {
open = $bindable(true),
members = $bindable<UserWithStatus[]>([]),
user,
currentEntityId = $bindable<string | null>(null)
}: {
open: boolean;
members: UserWithStatus[];
user: SessionValidationResult['user'];
currentEntityId: string | null;
} = $props();
let sidebar_probably = useSidebar();
$effect(() => {
if (sidebar_probably.open != open) {
sidebar_probably.setOpen(open);
}
});
</script>
<Sidebar.Root side="right">
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Members</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each members as member (member.id)}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<User user={member} />
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{#if currentEntityId && ServerID.is(currentEntityId) && user}
<Sidebar.Group>
<Sidebar.GroupLabel>Server Actions</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<form method="POST" action="?/inviteUser" class="w-full">
<input type="hidden" name="serverId" value={currentEntityId} />
<button type="submit" class="flex w-full items-center gap-2" {...props}>
<UserRoundPlus class="size-4" />
<span>Invite User</span>
</button>
</form>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{#if user.id === members.find((m) => m.id === currentEntityId)?.ownerId}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<form method="POST" action="?/leaveServer" class="w-full">
<input type="hidden" name="serverId" value={currentEntityId} />
<button type="submit" class="flex w-full items-center gap-2" {...props}>
<UserRoundMinus class="size-4" />
<span>Leave Server</span>
</button>
</form>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/if}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}
</Sidebar.Content>
</Sidebar.Root>

View file

@ -0,0 +1,58 @@
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { eq, inArray } from 'drizzle-orm';
import { GroupID, ServerID } from '$lib';
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const { entityId } = params;
if (!entityId) {
throw error(400, 'Entity ID is required');
}
if (GroupID.is(entityId)) {
const group = await db.select().from(table.group).where(eq(table.group.id, entityId));
if (!group || group?.length == 0) {
throw error(404, 'Group not found');
}
const members = await db
.select()
.from(table.user)
.where(inArray(table.user.id, group[0].members as string[]));
return json({
members: members.map((member) => ({
id: member.id,
username: member.username,
image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}`
}))
});
}
if (ServerID.is(entityId)) {
const server = await db.select().from(table.server).where(eq(table.server.id, entityId));
if (!server || server?.length == 0) {
throw error(404, 'Server not found');
}
const members = await db
.select()
.from(table.user)
.where(inArray(table.user.id, server[0].members as string[]));
return json({
members: members.map((member) => ({
id: member.id,
username: member.username,
image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}`
}))
});
}
throw error(400, 'Invalid entity ID');
};

View file

@ -24,13 +24,19 @@
import Input from '$lib/components/ui/input/input.svelte';
import { Button } from '$lib/components/ui/button';
import { SendHorizontal } from '@lucide/svelte';
import SendHorizontal from '@lucide/svelte/icons/send-horizontal';
import PersonStanding from '@lucide/svelte/icons/person-standing';
import MemberSidebar from '$lib/components/member-sidebar.svelte';
let errorOpen = $state(true);
let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
let isMembersTabOpen = $state(true);
let members: UserWithStatus[] = $state([]);
let messages: ReturnMessage[] = $state([]);
let inputValue = $state();
@ -57,6 +63,20 @@
}
});
$effect(() => {
if (!currentPageID || !currentPage) return;
if (ServerID.is(currentPageID) || GroupID.is(currentPageID)) {
async function fetchMembers() {
const req = await fetch(`/api/members/${currentPageID}`);
const data = await req.json();
members = data.members;
}
fetchMembers();
} else {
isMembersTabOpen = false;
}
});
$effect(() => {
if (!currentPageID || !currentPage) return;
if (ServerID.is(currentPageID)) return;
@ -96,7 +116,6 @@
id: ServerID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
});
@ -180,7 +199,6 @@
<Sidebar.Provider>
<AppSidebar bind:currentPage={currentPageID} user={data.user} data={overview_data} />
<Sidebar.Inset class="h-svh">
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<Sidebar.Trigger class="-ms-1" />
@ -210,6 +228,19 @@
<h1>{group!.name} ({group.members} member{group.members > 1 ? 's' : ''})</h1>
{/if}
{#if ServerID.is(currentPageID) || GroupID.is(currentPageID)}
<Button
variant="outline"
onclick={() => {
isMembersTabOpen = !isMembersTabOpen;
}}
>
<PersonStanding></PersonStanding>
<PersonStanding></PersonStanding>
<PersonStanding></PersonStanding>
<PersonStanding></PersonStanding>
</Button>
{/if}
{/if}
</header>
<div class="h-min shrink overflow-scroll">
@ -288,4 +319,14 @@
</div>
{/if}
</Sidebar.Inset>
<Sidebar.Provider class="w-0">
<Sidebar.Provider class="w-0">
<MemberSidebar
bind:open={isMembersTabOpen}
user={data.user}
{members}
currentEntityId={currentPageID}
/>
</Sidebar.Provider>
</Sidebar.Provider>
</Sidebar.Provider>