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 {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
color: user.color,
|
color: user.color,
|
||||||
|
status: user.status,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -70,7 +71,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
jwt({ token, user }) {
|
jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id;
|
token.id = user.id;
|
||||||
|
token.username = user.username;
|
||||||
token.color = user.color;
|
token.color = user.color;
|
||||||
|
token.status = user.status;
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
|
|
@ -85,7 +88,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
session({ session, token }) {
|
session({ session, token }) {
|
||||||
if (token && session.user) {
|
if (token && session.user) {
|
||||||
session.user.id = token.id as string;
|
session.user.id = token.id as string;
|
||||||
|
session.user.username = token.username as string;
|
||||||
session.user.color = token.color as string;
|
session.user.color = token.color as string;
|
||||||
|
session.user.status = token.status as any;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Message, Member, User } from "@/db/schema";
|
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.
|
* 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 }) {
|
export function ChatItem({ message }: { message: MessageWithMember }) {
|
||||||
const user = message.member?.user;
|
const user = message.member?.user;
|
||||||
const fullName = user ? user.username.trim() : "Deleted Member";
|
const fullName = user ? user.username.trim() : "Deleted Member";
|
||||||
const initial = user?.username?.charAt(0) || "?";
|
|
||||||
|
|
||||||
const formattedTime = new Date(message.createdAt).toLocaleTimeString(
|
const formattedTime = new Date(message.createdAt).toLocaleTimeString(
|
||||||
"de-DE",
|
"de-DE",
|
||||||
|
|
@ -38,14 +38,8 @@ export function ChatItem({ message }: { message: MessageWithMember }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 group p-2 rounded-xl hover:bg-surface transition-colors">
|
<div className="flex items-start gap-3 group p-2 rounded-xl hover:bg-surface transition-colors">
|
||||||
{/* Avatar */}
|
{/* Avatar Component */}
|
||||||
<div
|
<UserAvatar user={user} size="md" />
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Message Header & Content */}
|
{/* Message Header & Content */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { UserStatus } from "@/db/schema";
|
import type { UserStatus } from "@/db/schema";
|
||||||
import {
|
|
||||||
MEMBER_COLOR_CLASSES,
|
|
||||||
MEMBER_STATUS_COLOR_CLASSES,
|
|
||||||
} from "@/lib/constants/member.styles";
|
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
import { UserAvatar } from "../ui/UserAvatar";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing a member user.
|
* Interface representing a member user.
|
||||||
|
|
@ -45,9 +42,6 @@ interface MemberItemProps {
|
||||||
* @returns {JSX.Element} The rendered member item.
|
* @returns {JSX.Element} The rendered member item.
|
||||||
*/
|
*/
|
||||||
export function MemberItem({ member, isOffline = false }: MemberItemProps) {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
@ -56,23 +50,10 @@ export function MemberItem({ member, isOffline = false }: MemberItemProps) {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative shrink-0">
|
<div className="relative shrink-0">
|
||||||
<div
|
<UserAvatar user={member} size="md" />
|
||||||
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,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</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 || "Unbekannt"}
|
{member.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,48 +7,38 @@
|
||||||
|
|
||||||
import { LogOut, Settings } from "lucide-react";
|
import { LogOut, Settings } from "lucide-react";
|
||||||
import { signOut, useSession } from "next-auth/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() {
|
export function UserPanel() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
/**
|
if (!session?.user) return null;
|
||||||
* Triggers the NextAuth sign-out procedure and redirects the user to the login page.
|
|
||||||
*/
|
|
||||||
const handleLogout = () => {
|
|
||||||
signOut({ callbackUrl: "/login" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const username = session?.user?.name || "User";
|
const { user } = session;
|
||||||
const userInitial = username.charAt(0).toUpperCase();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2 w-full bg-surface shrink-0">
|
<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">
|
<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="flex items-center gap-2.5 min-w-0 flex-1 cursor-pointer group">
|
||||||
<div className="relative shrink-0">
|
<UserAvatar
|
||||||
<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">
|
user={user}
|
||||||
{userInitial}
|
size="sm"
|
||||||
</div>
|
className="transition-transform group-hover:scale-105"
|
||||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-emerald-500 ring-2 ring-background" />
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-semibold text-foreground truncate leading-tight group-hover:text-accent transition-colors">
|
<p className="text-sm font-semibold text-foreground truncate leading-tight group-hover:text-accent transition-colors">
|
||||||
{username}
|
{user.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted truncate leading-tight font-medium">
|
<p className="text-xs text-muted truncate leading-tight font-medium capitalize">
|
||||||
Online
|
{user.status.toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="p-1.5 text-muted hover:text-foreground focus:outline-none cursor-pointer transition-colors"
|
className="p-1.5 text-muted hover:text-foreground focus:outline-none cursor-pointer transition-colors"
|
||||||
aria-label="User Settings"
|
aria-label="User Settings"
|
||||||
>
|
>
|
||||||
|
|
@ -56,7 +46,8 @@ export function UserPanel() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
className="p-1.5 text-muted hover:text-red-400 focus:outline-none cursor-pointer transition-colors"
|
||||||
aria-label="Log Out"
|
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 { DefaultSession } from "next-auth";
|
||||||
|
import type { UserStatus } from "@/db/schema";
|
||||||
|
|
||||||
declare module "next-auth" {
|
declare module "next-auth" {
|
||||||
/**
|
/**
|
||||||
* Extends the default NextAuth Session interface to include custom user properties.
|
* 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 {
|
interface Session {
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
|
username: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
status: UserStatus;
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extends the default NextAuth User interface to include custom properties.
|
* 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 {
|
interface User {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
username?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
status?: UserStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue