feat(members): add role badges to member list items
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 7m6s

This commit is contained in:
Chneemann 2026-09-17 17:22:07 +02:00
parent 4622dac9bf
commit 009c1ef9de
No known key found for this signature in database
4 changed files with 54 additions and 24 deletions

View file

@ -1,27 +1,30 @@
/** /**
* @file components/members/MemberItem.tsx * @file components/members/MemberItem.tsx
* @description Member item component rendering user details with popover toggle for profile views and direct actions. * @description Member item component rendering user details with role badge and popover toggle.
*/ */
"use client"; "use client";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { User } from "@/db/schema";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { UserAvatar } from "../ui/UserAvatar"; import { UserAvatar } from "../ui/UserAvatar";
import { UserProfilePopover } from "../ui/UserProfilePopover"; import { UserProfilePopover } from "../ui/UserProfilePopover";
import { useActiveServer } from "@/lib/context/ServerContext"; import {
ServerMemberWithUser,
useActiveServer,
} from "@/lib/context/ServerContext";
import { useSidebarStore } from "@/lib/stores/useSidebarStore"; import { useSidebarStore } from "@/lib/stores/useSidebarStore";
import { useUser } from "@/lib/context/UserContext"; import { useUser } from "@/lib/context/UserContext";
import { Crown, Shield } from "lucide-react";
/** Props for the MemberItem component. */ /** Props for the MemberItem component. */
interface MemberItemProps { interface MemberItemProps {
member: User; member: ServerMemberWithUser;
isOffline?: boolean; isOffline?: boolean;
} }
/** Renders an individual member item with an interactive user profile popover. */ /** Renders an individual member item with an interactive user profile popover and role badge. */
export function MemberItem({ member, isOffline = false }: MemberItemProps) { export function MemberItem({ member, isOffline = false }: MemberItemProps) {
const router = useRouter(); const router = useRouter();
const { setActiveServer } = useActiveServer(); const { setActiveServer } = useActiveServer();
@ -121,16 +124,28 @@ export function MemberItem({ member, isOffline = false }: MemberItemProps) {
type="button" type="button"
onClick={() => setIsProfileOpen((prev) => !prev)} onClick={() => setIsProfileOpen((prev) => !prev)}
className={clsx( className={clsx(
"w-full flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-background/50 transition-colors cursor-pointer group text-left focus:outline-none", "w-full flex items-center justify-between px-2 py-1.5 rounded-md hover:bg-background/50 transition-colors cursor-pointer group text-left focus:outline-none",
isOffline && "opacity-60", isOffline && "opacity-60",
)} )}
> >
<div className="flex items-center gap-2.5 min-w-0">
<div className="relative shrink-0"> <div className="relative shrink-0">
<UserAvatar user={member} size="md" /> <UserAvatar user={member} size="md" />
</div> </div>
<span className="text-sm font-medium text-muted group-hover:text-white truncate"> <span className="text-sm font-medium text-muted group-hover:text-white truncate">
{member.username} {member.username}
</span> </span>
</div>
{/* Rolle-Badge/Icon */}
<div className="shrink-0 ml-1">
{member.role === "OWNER" && (
<Crown className="w-4 h-4 text-amber-400" />
)}
{member.role === "ADMIN" && (
<Shield className="w-4 h-4 text-indigo-400" />
)}
</div>
</button> </button>
{isProfileOpen && ( {isProfileOpen && (

View file

@ -5,14 +5,14 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { MemberItem } from "@/components/members/MemberItem"; import { MemberItem } from "@/components/members/MemberItem";
import { User } from "@/db/schema"; import { ServerMemberWithUser } from "@/lib/context/ServerContext";
interface MemberListProps {
members: User[];
}
/** Renders online and offline community members in distinct sections. */ /** Renders online and offline community members in distinct sections. */
export function MemberList({ members = [] }: MemberListProps) { export function MemberList({
members = [],
}: {
members?: ServerMemberWithUser[];
}) {
const { onlineMembers, offlineMembers } = useMemo(() => { const { onlineMembers, offlineMembers } = useMemo(() => {
return { return {
onlineMembers: members.filter((m) => m.status !== "OFFLINE"), onlineMembers: members.filter((m) => m.status !== "OFFLINE"),

View file

@ -8,14 +8,19 @@
import { createContext, useContext, useState } from "react"; import { createContext, useContext, useState } from "react";
import type { Server, Channel, Category, User, Member } from "@/db/schema"; import type { Server, Channel, Category, User, Member } from "@/db/schema";
/** Extended server type containing associated channels and categories. */ /** Member type combining User details with server Role. */
export type ServerMemberWithUser = User & {
role: Member["role"];
};
/** Server type including associated channels, categories, and members. */
export type ServerWithChannels = Server & { export type ServerWithChannels = Server & {
channels: Channel[]; channels: Channel[];
categories: Category[]; categories: Category[];
members: User[]; members: ServerMemberWithUser[];
}; };
/** Member representation within a server context. */ /** Simplified member representation within server context. */
export interface ServerMember { export interface ServerMember {
id: string; id: string;
name: string; name: string;

View file

@ -10,7 +10,7 @@ import { z } from "zod";
const uuidSchema = z.uuid(); const uuidSchema = z.uuid();
/** Retrieves a server along with its channels, categories and members sorted chronologically if the specified user is a verified member. */ /** Retrieves a server along with channels, categories, and members (with role). */
export async function getUserServer(serverId: string, userId: string) { export async function getUserServer(serverId: string, userId: string) {
if ( if (
!uuidSchema.safeParse(serverId).success || !uuidSchema.safeParse(serverId).success ||
@ -47,7 +47,12 @@ export async function getUserServer(serverId: string, userId: string) {
return { return {
...server, ...server,
members: server.members.map((m) => m.user).filter(Boolean), members: server.members
.filter((m) => Boolean(m.user))
.map((m) => ({
...m.user,
role: m.role,
})),
}; };
} catch (error) { } catch (error) {
console.error(`Error fetching server ${serverId}:`, error); console.error(`Error fetching server ${serverId}:`, error);
@ -55,7 +60,7 @@ export async function getUserServer(serverId: string, userId: string) {
} }
} }
/** Fetches all servers that the specified user belongs to, including each server's sorted channels, categories, and members. */ /** Fetches all servers for a user with channels, categories, and members (with role). */
export async function getUserServers(userId: string) { export async function getUserServers(userId: string) {
if (!uuidSchema.safeParse(userId).success) { if (!uuidSchema.safeParse(userId).success) {
return []; return [];
@ -88,7 +93,12 @@ export async function getUserServers(userId: string) {
if (!membership.server) return null; if (!membership.server) return null;
return { return {
...membership.server, ...membership.server,
members: membership.server.members.map((m) => m.user).filter(Boolean), members: membership.server.members
.filter((m) => Boolean(m.user))
.map((m) => ({
...m.user,
role: m.role,
})),
}; };
}) })
.filter((server): server is NonNullable<typeof server> => .filter((server): server is NonNullable<typeof server> =>