refactor(ui): extract UserAvatar component and extend NextAuth session types
This commit is contained in:
parent
08513015d3
commit
0e6ee4dab2
6 changed files with 117 additions and 64 deletions
7
auth.ts
7
auth.ts
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
86
components/ui/UserAvatar.tsx
Normal file
86
components/ui/UserAvatar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
lib/types/next-auth.d.ts
vendored
14
lib/types/next-auth.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue