feat(members): add role badges to member list items
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 7m6s
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 7m6s
This commit is contained in:
parent
4622dac9bf
commit
009c1ef9de
4 changed files with 54 additions and 24 deletions
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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> =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue