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
* @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";
import { useState, useRef, useEffect } from "react";
import { useRouter } from "next/navigation";
import type { User } from "@/db/schema";
import { clsx } from "clsx";
import { UserAvatar } from "../ui/UserAvatar";
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 { useUser } from "@/lib/context/UserContext";
import { Crown, Shield } from "lucide-react";
/** Props for the MemberItem component. */
interface MemberItemProps {
member: User;
member: ServerMemberWithUser;
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) {
const router = useRouter();
const { setActiveServer } = useActiveServer();
@ -121,16 +124,28 @@ export function MemberItem({ member, isOffline = false }: MemberItemProps) {
type="button"
onClick={() => setIsProfileOpen((prev) => !prev)}
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",
)}
>
<div className="relative shrink-0">
<UserAvatar user={member} size="md" />
<div className="flex items-center gap-2.5 min-w-0">
<div className="relative shrink-0">
<UserAvatar user={member} size="md" />
</div>
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
{member.username}
</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>
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
{member.username}
</span>
</button>
{isProfileOpen && (

View file

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

View file

@ -8,14 +8,19 @@
import { createContext, useContext, useState } from "react";
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 & {
channels: Channel[];
categories: Category[];
members: User[];
members: ServerMemberWithUser[];
};
/** Member representation within a server context. */
/** Simplified member representation within server context. */
export interface ServerMember {
id: string;
name: string;

View file

@ -10,7 +10,7 @@ import { z } from "zod";
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) {
if (
!uuidSchema.safeParse(serverId).success ||
@ -47,7 +47,12 @@ export async function getUserServer(serverId: string, userId: string) {
return {
...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) {
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) {
if (!uuidSchema.safeParse(userId).success) {
return [];
@ -88,7 +93,12 @@ export async function getUserServers(userId: string) {
if (!membership.server) return null;
return {
...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> =>