Files
frontend/src/components/message/ChatHeader.tsx

211 lines
6.8 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import Image from 'next/image';
import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu';
interface ChatHeaderProps {
name: string;
role: string;
lastSeen: string;
avatar: string;
isOnline?: boolean;
pronouns?: string;
expertise?: string[];
isTyping?: boolean;
typingText?: string;
isMuted?: boolean;
isStarred?: boolean;
onClearChat?: () => void;
onDeleteConversation?: () => void;
onReportUser?: () => void;
onToggleMute?: (muted: boolean) => void;
onToggleStar?: (starred: boolean) => void;
}
export function ChatHeader({
name,
role,
lastSeen,
avatar,
isOnline = false,
pronouns,
expertise = [],
isTyping = false,
typingText,
isMuted = false,
isStarred = false,
onClearChat,
onDeleteConversation,
onReportUser,
onToggleMute,
onToggleStar,
}: ChatHeaderProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [avatarLoaded, setAvatarLoaded] = useState(false);
const placeholder = '/assets/icons/user-placeholder-icon.svg';
const isPlaceholderAvatar = !avatar || avatar === placeholder;
const menuItems: DropdownMenuItem[] = [
{
label: 'Clear Chat',
onClick: () => onClearChat?.(),
},
{
label: isMuted ? 'Unmute Notifications' : 'Mute Notifications',
onClick: () => onToggleMute?.(!isMuted),
},
{
label: 'Report User',
onClick: () => onReportUser?.(),
},
{
label: 'Delete Conversation',
onClick: () => onDeleteConversation?.(),
danger: true,
},
];
const handleToggleMenu = useCallback(() => {
setIsMenuOpen((prev) => !prev);
}, []);
const handleCloseMenu = useCallback(() => {
setIsMenuOpen(false);
}, []);
return (
<div className="border-b border-[#00293d]/10 pb-4">
{/* Top row - Name, status, actions */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2 min-w-0 flex-1">
<h2 className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] truncate">
{name}
</h2>
<span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isOnline ? 'bg-green-500' : 'bg-gray-400'}`} />
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] hidden sm:inline">
{role}
</span>
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] hidden sm:inline">
{isTyping ? (
<span className="text-[#e58625] italic">{typingText}</span>
) : (
lastSeen
)}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Mute indicator */}
{isMuted && (
<div className="flex items-center gap-1 px-2 py-1 bg-[#00293D]/10 rounded-lg">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" className="text-[#00293D]/60">
<path d="M11 5L6 9H2v6h4l5 4V5zM23 9l-6 6M17 9l6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span className="font-serif text-[11px] text-[#00293D]/60">Muted</span>
</div>
)}
<div className="relative">
<button
onClick={handleToggleMenu}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
>
<Image
src="/assets/icons/three-dots-horizontal-icon.svg"
alt="More options"
width={24}
height={24}
/>
</button>
<ChatDropdownMenu
items={menuItems}
isOpen={isMenuOpen}
onClose={handleCloseMenu}
/>
</div>
<button
onClick={() => onToggleStar?.(!isStarred)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
>
{isStarred ? (
<Image
src="/assets/icons/star-filled-icon.svg"
alt="Unstar conversation"
width={24}
height={24}
/>
) : (
<Image
src="/assets/icons/star-outline-icon.svg"
alt="Star conversation"
width={24}
height={24}
/>
)}
</button>
</div>
</div>
{/* User profile row */}
<div className="flex items-start gap-3">
{/* Avatar */}
<div className="relative flex-shrink-0">
<div className="w-[50px] h-[50px] rounded-full overflow-hidden border border-[#00293D]/20 relative">
{(!avatar || !avatarLoaded || isPlaceholderAvatar) && (
<div className="absolute inset-0 rounded-full shimmer-loading" />
)}
{avatar && !isPlaceholderAvatar && (
<img
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }}
src={avatar}
alt={name}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setAvatarLoaded(true)}
/>
)}
</div>
{isOnline && (
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white" />
)}
</div>
{/* User info */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D]">
{name}
</span>
<Image
src="/assets/icons/shield-verified-icon.svg"
alt="Verified"
width={17}
height={18}
/>
{pronouns && (
<span className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D]">
({pronouns})
</span>
)}
</div>
{/* Expertise tags */}
{expertise.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
{expertise.map((tag, index) => (
<span key={index} className="flex items-center">
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D]">
{tag}
</span>
{index < expertise.length - 1 && (
<span className="mx-2 text-[#00293d]/30">|</span>
)}
</span>
))}
</div>
)}
</div>
</div>
</div>
);
}