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

@ -1 +0,0 @@
The files here are not for use. I use them as inspiration.

View file

@ -1,50 +0,0 @@
<script lang="ts">
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { Avatar, AvatarImage, AvatarFallback } from "$lib/components/ui/avatar/index.js";
import UserIcon from "@lucide/svelte/icons/user";
// Sample members data
const members = [
{ id: 1, name: "Alice", status: "online", image: "https://placehold.co/40x40" },
{ id: 2, name: "Bob", status: "online", image: "https://placehold.co/40x40" },
{ id: 3, name: "Charlie", status: "offline", image: "https://placehold.co/40x40" },
{ id: 4, name: "Diana", status: "online", image: "https://placehold.co/40x40" },
{ id: 5, name: "Eve", status: "dnd", image: "https://placehold.co/40x40" },
];
// Status colors
const statusColors = {
online: "bg-green-500",
offline: "bg-gray-500",
dnd: "bg-red-500"
};
</script>
<Sidebar.Root class="w-64 border-l">
<Sidebar.Header class="p-4 border-b">
<h3 class="font-medium">Members ({members.length})</h3>
</Sidebar.Header>
<Sidebar.Content class="p-2">
<Sidebar.Group>
<Sidebar.Menu>
{#each members as member (member.id)}
<Sidebar.MenuItem>
<Sidebar.MenuButton class="w-full justify-start gap-3">
<div class="relative">
<Avatar class="size-8">
<AvatarImage src={member.image} alt={member.name} />
<AvatarFallback>
<UserIcon class="size-4" />
</AvatarFallback>
</Avatar>
<span class={`absolute bottom-0 right-0 block size-2 rounded-full ring-1 ring-white ${statusColors[member.status]}`}></span>
</div>
<span class="truncate">{member.name}</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>

View file

@ -1,21 +0,0 @@
<script lang="ts">
import { Label } from "$lib/components/ui/label/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import type { WithElementRef } from "$lib/utils.js";
import SearchIcon from "@lucide/svelte/icons/search";
import type { HTMLFormAttributes } from "svelte/elements";
let { ref = $bindable(null), ...restProps }: WithElementRef<HTMLFormAttributes> = $props();
</script>
<form bind:this={ref} {...restProps}>
<Sidebar.Group class="py-0">
<Sidebar.GroupContent class="relative">
<Label for="search" class="sr-only">Search</Label>
<Sidebar.Input id="search" placeholder="Search the docs..." class="ps-8" />
<SearchIcon
class="pointer-events-none absolute start-2 top-1/2 size-4 -translate-y-1/2 opacity-50 select-none"
/>
</Sidebar.GroupContent>
</Sidebar.Group>
</form>

View file

@ -1,28 +0,0 @@
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog/index.js";
export let open: boolean = false;
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Description>
Configure your application settings
</Dialog.Description>
</Dialog.Header>
<div class="py-4">
<h1 class="text-lg font-medium">hi</h1>
</div>
<Dialog.Footer>
<Dialog.Close asChild let:builder>
<button use:builder class="bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 rounded">
Close
</button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

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>