add members sidebar + endpoint
This commit is contained in:
parent
d333cbff04
commit
6f3ceb6838
7 changed files with 195 additions and 103 deletions
|
|
@ -1 +0,0 @@
|
|||
The files here are not for use. I use them as inspiration.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
93
src/lib/components/member-sidebar.svelte
Normal file
93
src/lib/components/member-sidebar.svelte
Normal 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>
|
||||
58
src/routes/api/members/[entityId]/+server.ts
Normal file
58
src/routes/api/members/[entityId]/+server.ts
Normal 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');
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue