211 lines
6.8 KiB
TypeScript
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>
|
|
);
|
|
}
|