1-
1+ 'use client' ;
2+
3+ import { useState } from 'react' ;
4+ import { formatDistanceToNow } from 'date-fns' ;
5+ import {
6+ FiSearch ,
7+ FiMessageCircle ,
8+ } from 'react-icons/fi' ;
9+ import type { Conversation } from '@/app/store/messagingStore' ;
10+
11+ interface ConversationListProps {
12+ conversations : Conversation [ ] ;
13+ currentConversationId : string | null ;
14+ isLoading : boolean ;
15+ searchQuery : string ;
16+ onSelectConversation : ( id : string ) => void ;
17+ onSearchChange : ( query : string ) => void ;
18+ }
19+
20+ function UnreadIndicator ( { count } : { count : number } ) {
21+ if ( count === 0 ) return null ;
22+ return (
23+ < span className = "inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-bold text-white bg-gradient-to-r from-violet-500 to-purple-600 rounded-full shadow-sm shadow-purple-500/30 animate-pulse" >
24+ { count > 99 ? '99+' : count }
25+ </ span >
26+ ) ;
27+ }
28+
29+ function getInitials ( name : string ) {
30+ return name
31+ . split ( ' ' )
32+ . map ( ( n ) => n [ 0 ] )
33+ . join ( '' )
34+ . toUpperCase ( )
35+ . slice ( 0 , 2 ) ;
36+ }
37+
38+ function getAvatarColor ( name : string ) {
39+ const colors = [
40+ 'from-violet-500 to-purple-600' ,
41+ 'from-blue-500 to-cyan-500' ,
42+ 'from-emerald-500 to-teal-500' ,
43+ 'from-amber-500 to-orange-500' ,
44+ 'from-rose-500 to-pink-500' ,
45+ 'from-indigo-500 to-blue-600' ,
46+ ] ;
47+ const index = name . charCodeAt ( 0 ) % colors . length ;
48+ return colors [ index ] ;
49+ }
50+
51+ export default function ConversationList ( {
52+ conversations,
53+ currentConversationId,
54+ isLoading,
55+ searchQuery,
56+ onSelectConversation,
57+ onSearchChange,
58+ } : ConversationListProps ) {
59+ const [ isFocused , setIsFocused ] = useState ( false ) ;
60+
61+ return (
62+ < div className = "flex flex-col h-full" >
63+ { /* Search Bar */ }
64+ < div className = "p-3" >
65+ < div
66+ className = { `relative flex items-center rounded-xl transition-all duration-300 ${ isFocused
67+ ? 'bg-white dark:bg-gray-700 shadow-md shadow-purple-500/10 ring-2 ring-purple-500/30'
68+ : 'bg-gray-100 dark:bg-gray-800'
69+ } `}
70+ >
71+ < FiSearch className = "absolute left-3 w-4 h-4 text-gray-400" />
72+ < input
73+ type = "text"
74+ placeholder = "Search conversations..."
75+ value = { searchQuery }
76+ onChange = { ( e ) => onSearchChange ( e . target . value ) }
77+ onFocus = { ( ) => setIsFocused ( true ) }
78+ onBlur = { ( ) => setIsFocused ( false ) }
79+ className = "w-full py-2.5 pl-10 pr-4 bg-transparent text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none"
80+ id = "conversation-search"
81+ />
82+ </ div >
83+ </ div >
84+
85+ { /* Conversations List */ }
86+ < div className = "flex-1 overflow-y-auto" >
87+ { isLoading ? (
88+ < div className = "space-y-2 p-3" >
89+ { [ ...Array ( 5 ) ] . map ( ( _ , i ) => (
90+ < div
91+ key = { i }
92+ className = "flex items-center gap-3 p-3 rounded-xl animate-pulse"
93+ >
94+ < div className = "w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700" />
95+ < div className = "flex-1 space-y-2" >
96+ < div className = "h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded" />
97+ < div className = "h-3 w-36 bg-gray-200 dark:bg-gray-700 rounded" />
98+ </ div >
99+ </ div >
100+ ) ) }
101+ </ div >
102+ ) : conversations . length === 0 ? (
103+ < div className = "flex flex-col items-center justify-center py-16 px-6 text-center" >
104+ < div className = "w-16 h-16 rounded-full bg-gradient-to-br from-violet-100 to-purple-100 dark:from-violet-900/30 dark:to-purple-900/30 flex items-center justify-center mb-4" >
105+ < FiMessageCircle className = "w-7 h-7 text-violet-500" />
106+ </ div >
107+ < p className = "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" >
108+ { searchQuery ? 'No conversations found' : 'No conversations yet' }
109+ </ p >
110+ < p className = "text-xs text-gray-400" >
111+ { searchQuery
112+ ? 'Try a different search term'
113+ : 'Start a new conversation to begin messaging' }
114+ </ p >
115+ </ div >
116+ ) : (
117+ < div className = "space-y-0.5 px-2" >
118+ { conversations . map ( ( conversation ) => {
119+ const otherParticipant = conversation . participants . find (
120+ ( p ) => p . id !== 'current-user'
121+ ) ;
122+ const isActive = currentConversationId === conversation . id ;
123+ const hasUnread = conversation . unreadCount > 0 ;
124+
125+ if ( ! otherParticipant ) return null ;
126+
127+ return (
128+ < button
129+ key = { conversation . id }
130+ onClick = { ( ) => onSelectConversation ( conversation . id ) }
131+ className = { `w-full flex items-center gap-3 p-3 rounded-xl transition-all duration-200 text-left group ${ isActive
132+ ? 'bg-gradient-to-r from-violet-50 to-purple-50 dark:from-violet-950/40 dark:to-purple-950/40 shadow-sm ring-1 ring-violet-200/50 dark:ring-violet-800/30'
133+ : hasUnread
134+ ? 'bg-white dark:bg-gray-800/50 hover:bg-gray-50 dark:hover:bg-gray-800'
135+ : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
136+ } `}
137+ id = { `conversation-${ conversation . id } ` }
138+ >
139+ { /* Avatar */ }
140+ < div className = "relative flex-shrink-0" >
141+ < div
142+ className = { `w-12 h-12 rounded-full bg-gradient-to-br ${ getAvatarColor (
143+ otherParticipant . name
144+ ) } flex items-center justify-center text-white text-sm font-semibold shadow-sm`}
145+ >
146+ { getInitials ( otherParticipant . name ) }
147+ </ div >
148+ { /* Online indicator */ }
149+ { otherParticipant . online && (
150+ < div className = "absolute bottom-0.5 right-0.5 w-3 h-3 rounded-full bg-emerald-400 border-2 border-white dark:border-gray-900 shadow-sm" />
151+ ) }
152+ </ div >
153+
154+ { /* Content */ }
155+ < div className = "flex-1 min-w-0" >
156+ < div className = "flex items-center justify-between mb-1" >
157+ < span
158+ className = { `text-sm truncate ${ hasUnread
159+ ? 'font-bold text-gray-900 dark:text-white'
160+ : 'font-medium text-gray-700 dark:text-gray-200'
161+ } `}
162+ >
163+ { otherParticipant . name }
164+ </ span >
165+ < span className = "text-xs text-gray-400 dark:text-gray-500 flex-shrink-0 ml-2" >
166+ { conversation . lastMessage
167+ ? formatDistanceToNow (
168+ new Date ( conversation . lastMessage . timestamp ) ,
169+ { addSuffix : false }
170+ )
171+ : '' }
172+ </ span >
173+ </ div >
174+ < div className = "flex items-center justify-between gap-2" >
175+ < p
176+ className = { `text-xs truncate ${ hasUnread
177+ ? 'text-gray-700 dark:text-gray-300 font-medium'
178+ : 'text-gray-400 dark:text-gray-500'
179+ } `}
180+ >
181+ { conversation . lastMessage
182+ ? conversation . lastMessage . senderId === 'current-user'
183+ ? `You: ${ conversation . lastMessage . content . replace ( / < [ ^ > ] * > / g, '' ) } `
184+ : conversation . lastMessage . content . replace ( / < [ ^ > ] * > / g, '' )
185+ : 'Start a conversation...' }
186+ </ p >
187+ < UnreadIndicator count = { conversation . unreadCount } />
188+ </ div >
189+ </ div >
190+
191+ { /* Role Badge */ }
192+ { otherParticipant . role === 'instructor' && (
193+ < div className = "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity" >
194+ < span className = "text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 font-medium" >
195+ Instructor
196+ </ span >
197+ </ div >
198+ ) }
199+ </ button >
200+ ) ;
201+ } ) }
202+ </ div >
203+ ) }
204+ </ div >
205+ </ div >
206+ ) ;
207+ }
0 commit comments