feat: Implement agent-specific contact page CTA, enhance avatar loading and message previews, and refine messaging read receipts with auto-mark functionality.

This commit is contained in:
pradeepkumar
2026-03-27 06:23:28 +05:30
parent 6871470a39
commit 76ab856c20
5 changed files with 49 additions and 18 deletions

View File

@@ -35,10 +35,14 @@ export function ConnectionCard({
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4">
{/* Avatar */} {/* Avatar */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div className="w-[80px] h-[80px] rounded-full overflow-hidden relative"> <div className="w-[80px] h-[80px] rounded-full overflow-hidden relative bg-[#e8e8e8]">
{(!avatar || !avatarLoaded || isPlaceholder) && ( {isPlaceholder ? (
<div className="absolute inset-0 flex items-center justify-center bg-[#c4d9d4]">
<Image src={placeholder} alt={name} width={40} height={40} className="opacity-50" />
</div>
) : !avatarLoaded ? (
<div className="absolute inset-0 rounded-full shimmer-loading" /> <div className="absolute inset-0 rounded-full shimmer-loading" />
)} ) : null}
{avatar && !isPlaceholder && ( {avatar && !isPlaceholder && (
<img <img
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }}

View File

@@ -49,10 +49,14 @@ export function InvitationCard({
<div className={`flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4 ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}> <div className={`flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4 ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}>
{/* Avatar */} {/* Avatar */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div className="w-[80px] h-[80px] rounded-full overflow-hidden relative"> <div className="w-[80px] h-[80px] rounded-full overflow-hidden relative bg-[#e8e8e8]">
{(!avatar || !avatarLoaded || isPlaceholder) && ( {isPlaceholder ? (
<div className="absolute inset-0 flex items-center justify-center bg-[#c4d9d4]">
<Image src={placeholder} alt={name} width={40} height={40} className="opacity-50" />
</div>
) : !avatarLoaded ? (
<div className="absolute inset-0 rounded-full shimmer-loading" /> <div className="absolute inset-0 rounded-full shimmer-loading" />
)} ) : null}
{avatar && !isPlaceholder && ( {avatar && !isPlaceholder && (
<img <img
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }}

View File

@@ -3,6 +3,7 @@
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { CommonHeader } from '@/components/layout/CommonHeader'; import { CommonHeader } from '@/components/layout/CommonHeader';
import { Footer } from '@/components/layout/Footer'; import { Footer } from '@/components/layout/Footer';
import { cmsService } from '@/services/cms.service'; import { cmsService } from '@/services/cms.service';
@@ -47,6 +48,9 @@ const defaultCta: ContactCta = {
}; };
export default function ContactPage() { export default function ContactPage() {
const { data: session } = useSession();
const isAgent = (session?.user as any)?.role === 'AGENT';
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
phone: '', phone: '',
@@ -345,21 +349,21 @@ export default function ContactPage() {
</div> </div>
</div> </div>
{/* CTA Section - Ready to find an agent */} {/* CTA Section */}
<div className="max-w-4xl mx-auto border border-[#00293d]/20 rounded-[15px] p-8 lg:p-10 mb-10"> <div className="max-w-4xl mx-auto border border-[#00293d]/20 rounded-[15px] p-8 lg:p-10 mb-10">
<div className="flex flex-col lg:flex-row items-center justify-between gap-6"> <div className="flex flex-col lg:flex-row items-center justify-between gap-6">
<div className="lg:max-w-[400px]"> <div className="lg:max-w-[400px]">
<h2 className="font-fractul font-bold text-[24px] lg:text-[28px] text-[#00293d] mb-3"> <h2 className="font-fractul font-bold text-[24px] lg:text-[28px] text-[#00293d] mb-3">
{cta.title} {isAgent ? 'Manage your profile' : cta.title}
</h2> </h2>
<p className="font-serif text-[13px] text-[#00293d] mb-5"> <p className="font-serif text-[13px] text-[#00293d] mb-5">
{cta.description} {isAgent ? 'Keep your profile updated and connect with potential clients.' : cta.description}
</p> </p>
<Link <Link
href={cta.buttonLink} href={isAgent ? '/agent/dashboard' : cta.buttonLink}
className="inline-flex items-center gap-2 bg-[#e58625] text-white px-5 py-2.5 rounded-full font-fractul font-medium text-[13px] hover:bg-[#d47720] transition-colors" className="inline-flex items-center gap-2 bg-[#e58625] text-white px-5 py-2.5 rounded-full font-fractul font-medium text-[13px] hover:bg-[#d47720] transition-colors"
> >
{cta.buttonText} {isAgent ? 'Go to Dashboard' : cta.buttonText}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>

View File

@@ -825,7 +825,13 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
)} )}
</div> </div>
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] line-clamp-2"> <p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] line-clamp-2">
{conversation.lastMessageText || 'No messages yet'} {conversation.lastMessageText
? /giphy\.com|\.gif(\?|$)/i.test(conversation.lastMessageText)
? 'GIF 🎬'
: /\.(jpg|jpeg|png|webp|svg)(\?|$)/i.test(conversation.lastMessageText) && conversation.lastMessageText.startsWith('http')
? 'Image 📷'
: conversation.lastMessageText
: 'No messages yet'}
</p> </p>
</div> </div>

View File

@@ -480,6 +480,17 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
if (prev.some((m) => m.id === message.id)) return prev; if (prev.some((m) => m.id === message.id)) return prev;
return [...prev, message]; return [...prev, message];
}); });
// Auto-mark as read if user is currently viewing this conversation
// (the other user sent a message while we're looking at the chat)
const currentUserId = (session?.user as any)?.id;
if (message.senderId !== currentUserId) {
if (isConnected) {
socketService.markAsRead(message.conversationId);
} else {
messagesService.markAsRead(message.conversationId).catch(() => {});
}
}
} }
// Update conversation list // Update conversation list
@@ -536,13 +547,15 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
const unsubscribeRead = socketService.onMessagesRead( const unsubscribeRead = socketService.onMessagesRead(
(data: MessagesReadEvent) => { (data: MessagesReadEvent) => {
if (data.conversationId === currentConversationIdRef.current) { if (data.conversationId === currentConversationIdRef.current) {
// Update message statuses const currentUserId = (session?.user as any)?.id;
// Only update status on messages SENT by current user (not received ones)
setMessages((prev) => setMessages((prev) =>
prev.map((m) => ({ prev.map((m) => {
...m, if (m.senderId === currentUserId && m.status !== 'READ') {
status: 'READ' as MessageStatus, return { ...m, status: 'READ' as MessageStatus, readAt: data.readAt };
readAt: data.readAt, }
})) return m;
})
); );
} }
} }