refactor(ui): extract UserAvatar component and extend NextAuth session types

This commit is contained in:
Chneemann 2026-08-30 15:37:28 +02:00
parent 08513015d3
commit 0e6ee4dab2
No known key found for this signature in database
6 changed files with 117 additions and 64 deletions

View file

@ -51,9 +51,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
return {
id: user.id,
name: user.username,
username: user.username,
email: user.email,
color: user.color,
status: user.status,
};
},
}),
@ -70,7 +71,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
jwt({ token, user }) {
if (user) {
token.id = user.id;
token.username = user.username;
token.color = user.color;
token.status = user.status;
}
return token;
},
@ -85,7 +88,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string;
session.user.username = token.username as string;
session.user.color = token.color as string;
session.user.status = token.status as any;
}
return session;
},

View file

@ -6,6 +6,7 @@
"use client";
import type { Message, Member, User } from "@/db/schema";
import { UserAvatar } from "../ui/UserAvatar";
/**
* Composite message type extending base database Message with populated member and user relation.
@ -26,7 +27,6 @@ export type MessageWithMember = Message & {
export function ChatItem({ message }: { message: MessageWithMember }) {
const user = message.member?.user;
const fullName = user ? user.username.trim() : "Deleted Member";
const initial = user?.username?.charAt(0) || "?";
const formattedTime = new Date(message.createdAt).toLocaleTimeString(
"de-DE",
@ -38,14 +38,8 @@ export function ChatItem({ message }: { message: MessageWithMember }) {
return (
<div className="flex items-start gap-3 group p-2 rounded-xl hover:bg-surface transition-colors">
{/* Avatar */}
<div
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold text-white shrink-0 ${
user?.color || "bg-indigo-500"
}`}
>
{initial}
</div>
{/* Avatar Component */}
<UserAvatar user={user} size="md" />
{/* Message Header & Content */}
<div className="flex-1 min-w-0">

View file

@ -4,11 +4,8 @@
*/
import type { UserStatus } from "@/db/schema";
import {
MEMBER_COLOR_CLASSES,
MEMBER_STATUS_COLOR_CLASSES,
} from "@/lib/constants/member.styles";
import { clsx } from "clsx";
import { UserAvatar } from "../ui/UserAvatar";
/**
* Interface representing a member user.
@ -45,9 +42,6 @@ interface MemberItemProps {
* @returns {JSX.Element} The rendered member item.
*/
export function MemberItem({ member, isOffline = false }: MemberItemProps) {
const userBg = member.color && MEMBER_COLOR_CLASSES[member.color];
const statusBg = MEMBER_STATUS_COLOR_CLASSES[member.status];
return (
<div
className={clsx(
@ -56,23 +50,10 @@ export function MemberItem({ member, isOffline = false }: MemberItemProps) {
)}
>
<div className="relative shrink-0">
<div
className={clsx(
"w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs text-white",
userBg,
)}
>
{member.username ? member.username.charAt(0).toUpperCase() : "?"}
</div>
<span
className={clsx(
"absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full ring-2 ring-surface",
statusBg,
)}
/>
<UserAvatar user={member} size="md" />
</div>
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
{member.username || "Unbekannt"}
{member.username}
</span>
</div>
);

View file

@ -7,48 +7,38 @@
import { LogOut, Settings } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import { UserAvatar } from "@/components/ui/UserAvatar";
/**
* Renders the user panel footer component showing current session profile details and authentication controls.
*
* @returns {JSX.Element} The rendered user panel component.
*/
export function UserPanel() {
const { data: session } = useSession();
/**
* Triggers the NextAuth sign-out procedure and redirects the user to the login page.
*/
const handleLogout = () => {
signOut({ callbackUrl: "/login" });
};
if (!session?.user) return null;
const username = session?.user?.name || "User";
const userInitial = username.charAt(0).toUpperCase();
const { user } = session;
return (
<div className="p-2 w-full bg-surface shrink-0">
<footer className="h-14 bg-background/80 hover:bg-background/90 border border-background/50 rounded-xl flex items-center justify-between px-3 gap-2 shadow-lg backdrop-blur-md transition-all duration-200">
<div className="flex items-center gap-2.5 min-w-0 flex-1 cursor-pointer group">
<div className="relative shrink-0">
<div className="w-8 h-8 rounded-full bg-accent flex items-center justify-center text-white font-bold text-xs shadow-sm transition-transform group-hover:scale-105">
{userInitial}
</div>
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-emerald-500 ring-2 ring-background" />
</div>
<UserAvatar
user={user}
size="sm"
className="transition-transform group-hover:scale-105"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground truncate leading-tight group-hover:text-accent transition-colors">
{username}
{user.username}
</p>
<p className="text-xs text-muted truncate leading-tight font-medium">
Online
<p className="text-xs text-muted truncate leading-tight font-medium capitalize">
{user.status.toLowerCase()}
</p>
</div>
</div>
<div className="flex items-center gap-1">
<button
type="button"
className="p-1.5 text-muted hover:text-foreground focus:outline-none cursor-pointer transition-colors"
aria-label="User Settings"
>
@ -56,7 +46,8 @@ export function UserPanel() {
</button>
<button
onClick={handleLogout}
type="button"
onClick={() => signOut({ callbackUrl: "/login" })}
className="p-1.5 text-muted hover:text-red-400 focus:outline-none cursor-pointer transition-colors"
aria-label="Log Out"
>

View file

@ -0,0 +1,86 @@
/**
* @file components/ui/UserAvatar.tsx
* @description Renders a user avatar with initials and an online status indicator badge.
*/
import type { UserStatus } from "@/db/schema";
import {
MEMBER_COLOR_CLASSES,
MEMBER_STATUS_COLOR_CLASSES,
} from "@/lib/constants/member.styles";
import { clsx } from "clsx";
/**
* Size variants available for the UserAvatar component.
*
* @type AvatarSize
*/
type AvatarSize = "sm" | "md";
/**
* User object properties required to render the avatar.
*
* @interface AvatarUser
* @property {string} username - The display name of the user used to derive initials.
* @property {string} color - Color key mapping to the avatar background style.
* @property {UserStatus} status - The current online status of the user.
*/
export interface AvatarUser {
username: string;
color: string;
status: UserStatus;
}
/**
* Properties for the UserAvatar component.
*
* @interface UserAvatarProps
* @property {AvatarUser} user - The user object containing display details and status.
* @property {AvatarSize} [size="sm"] - The display size variant of the avatar.
* @property {string} [className] - Optional custom CSS classes for styling adjustments.
*/
interface UserAvatarProps {
user: AvatarUser;
size?: AvatarSize;
className?: string;
}
const SIZE_MAP: Record<AvatarSize, { container: string; badge: string }> = {
sm: { container: "w-8 h-8 text-xs", badge: "w-2.5 h-2.5" },
md: { container: "w-10 h-10 text-sm", badge: "w-3 h-3" },
};
/**
* Renders an avatar element displaying user initials along with a status badge.
*
* @param {UserAvatarProps} props - Component properties.
* @returns {JSX.Element} The rendered UserAvatar element.
*/
export function UserAvatar({ user, size = "sm", className }: UserAvatarProps) {
const { container, badge } = SIZE_MAP[size];
const userBg = MEMBER_COLOR_CLASSES[user.color];
const statusBg = MEMBER_STATUS_COLOR_CLASSES[user.status];
return (
<div className={clsx("relative shrink-0", className)}>
<div
className={clsx(
container,
userBg,
"rounded-full flex items-center justify-center font-bold text-white shadow-sm",
)}
>
{user.username.charAt(0).toUpperCase()}
</div>
<span
className={clsx(
"absolute bottom-0 right-0 rounded-full ring-2 ring-surface",
badge,
statusBg,
)}
/>
</div>
);
}

View file

@ -4,32 +4,28 @@
*/
import { DefaultSession } from "next-auth";
import type { UserStatus } from "@/db/schema";
declare module "next-auth" {
/**
* Extends the default NextAuth Session interface to include custom user properties.
*
* @interface Session
* @property {Object} user - The session user object.
* @property {string} user.id - The unique identifier of the user.
* @property {string} user.color - The custom user interface color preference or avatar fallback color.
*/
interface Session {
user: {
id: string;
username: string;
color: string;
status: UserStatus;
} & DefaultSession["user"];
}
/**
* Extends the default NextAuth User interface to include custom properties.
*
* @interface User
* @property {string} [id] - The optional unique identifier of the user.
* @property {string} [color] - The optional custom color attribute associated with the user.
*/
interface User {
id?: string;
username?: string;
color?: string;
status?: UserStatus;
}
}