11'use client' ;
22
3- import React , { useMemo , useState , useEffect , useRef } from 'react' ;
3+ import React , { useMemo , useState , useEffect , useRef , useId } from 'react' ;
44import { motion , AnimatePresence } from 'framer-motion' ;
55import { Paperclip , Send } from 'lucide-react' ;
66import RichTextEditor from '@/app/components/ui/RichTextEditor' ;
@@ -26,6 +26,7 @@ export default function GroupDiscussionThread({ messages, onPost }: GroupDiscuss
2626 const [ files , setFiles ] = useState < File [ ] > ( [ ] ) ;
2727 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
2828 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
29+ const editorHelpId = useId ( ) ;
2930
3031 const attachments = useMemo < Attachment [ ] > (
3132 ( ) =>
@@ -56,37 +57,60 @@ export default function GroupDiscussionThread({ messages, onPost }: GroupDiscuss
5657 }
5758 } ;
5859
60+ const handleEditorKeyDown = ( event : React . KeyboardEvent < HTMLDivElement > ) => {
61+ if ( ( event . metaKey || event . ctrlKey ) && event . key === 'Enter' ) {
62+ event . preventDefault ( ) ;
63+ handlePost ( ) ;
64+ }
65+ } ;
66+
5967 return (
6068 < div className = "flex flex-col h-full max-h-[600px] bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700" >
61- < div className = "flex-1 overflow-y-auto space-y-4 p-4" >
69+ < div
70+ className = "flex-1 overflow-y-auto space-y-4 p-4"
71+ role = "log"
72+ aria-label = "Discussion messages"
73+ aria-live = "polite"
74+ aria-relevant = "additions text"
75+ >
6276 { messages . length === 0 ? (
63- < div className = "text-center py-8 text-gray-500 dark:text-gray-400" >
77+ < div className = "text-center py-8 text-gray-500 dark:text-gray-400" role = "status" >
6478 < p className = "text-sm" > No messages yet. Start the conversation!</ p >
6579 </ div >
6680 ) : (
6781 < AnimatePresence >
6882 { messages . map ( ( m , index ) => (
69- < motion . div
83+ < motion . article
7084 key = { m . id }
7185 initial = { { opacity : 0 , y : 10 } }
7286 animate = { { opacity : 1 , y : 0 } }
7387 transition = { { delay : index * 0.05 } }
7488 className = "flex items-start gap-3 group"
89+ aria-labelledby = { `message-${ m . id } -sender` }
7590 >
7691 { /* Avatar */ }
77- < div className = "flex-shrink-0 w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/30 border-2 border-white dark:border-gray-700 flex items-center justify-center text-sm font-medium text-purple-600 dark:text-purple-400" >
92+ < div
93+ className = "flex-shrink-0 w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/30 border-2 border-white dark:border-gray-700 flex items-center justify-center text-sm font-medium text-purple-600 dark:text-purple-400"
94+ aria-hidden = "true"
95+ >
7896 { getInitials ( m . senderName ) }
7997 </ div >
8098
8199 { /* Message Content */ }
82100 < div className = "flex-1 min-w-0" >
83101 < div className = "flex items-center gap-2 mb-1" >
84- < span className = "text-sm font-medium text-gray-900 dark:text-gray-50" >
102+ < span
103+ id = { `message-${ m . id } -sender` }
104+ className = "text-sm font-medium text-gray-900 dark:text-gray-50"
105+ >
85106 { m . senderName }
86107 </ span >
87- < span className = "text-xs text-gray-500 dark:text-gray-400" >
108+ < time
109+ className = "text-xs text-gray-500 dark:text-gray-400"
110+ dateTime = { new Date ( m . createdAt ) . toISOString ( ) }
111+ >
88112 { formatDistanceToNow ( new Date ( m . createdAt ) , { addSuffix : true } ) }
89- </ span >
113+ </ time >
90114 </ div >
91115 < div
92116 className = "prose prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"
@@ -109,16 +133,31 @@ export default function GroupDiscussionThread({ messages, onPost }: GroupDiscuss
109133 </ div >
110134 ) }
111135 </ div >
112- </ motion . div >
136+ </ motion . article >
113137 ) ) }
114138 </ AnimatePresence >
115139 ) }
116140 < div ref = { messagesEndRef } />
117141 </ div >
118142
119143 { /* Message Input */ }
120- < div className = "border-t border-gray-200 dark:border-gray-700 p-4 space-y-3 bg-gray-50 dark:bg-gray-900/50" >
121- < RichTextEditor content = { content } onChange = { setContent } placeholder = "Share an update..." />
144+ < form
145+ className = "border-t border-gray-200 dark:border-gray-700 p-4 space-y-3 bg-gray-50 dark:bg-gray-900/50"
146+ onSubmit = { ( event ) => {
147+ event . preventDefault ( ) ;
148+ handlePost ( ) ;
149+ } }
150+ aria-label = "Create discussion post"
151+ >
152+ < div onKeyDown = { handleEditorKeyDown } >
153+ < RichTextEditor
154+ content = { content }
155+ onChange = { setContent }
156+ placeholder = "Share an update..."
157+ ariaLabel = "Discussion post content"
158+ describedBy = { editorHelpId }
159+ />
160+ </ div >
122161 < div className = "flex items-center justify-between" >
123162 < label className = "inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 cursor-pointer hover:text-purple-600 dark:hover:text-purple-400 transition-colors" >
124163 < Paperclip size = { 16 } />
@@ -128,16 +167,17 @@ export default function GroupDiscussionThread({ messages, onPost }: GroupDiscuss
128167 type = "file"
129168 className = "hidden"
130169 multiple
170+ aria-label = "Attach files to discussion post"
131171 onChange = { ( e ) => {
132172 const fl = Array . from ( e . target . files || [ ] ) ;
133173 setFiles ( ( prev ) => [ ...prev , ...fl ] ) ;
134174 } }
135175 />
136176 </ label >
137177 < button
138- onClick = { handlePost }
139178 disabled = { ! content || content === '<p></p>' || content . trim ( ) === '' }
140179 className = "px-4 py-2 rounded-md bg-purple-600 hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white text-sm font-medium transition-colors flex items-center gap-2"
180+ type = "submit"
141181 >
142182 < Send size = { 16 } />
143183 Post
@@ -155,17 +195,21 @@ export default function GroupDiscussionThread({ messages, onPost }: GroupDiscuss
155195 >
156196 { file . name }
157197 < button
198+ type = "button"
158199 onClick = { ( ) => setFiles ( files . filter ( ( _ , i ) => i !== index ) ) }
159200 className = "hover:text-purple-900 dark:hover:text-purple-100"
201+ aria-label = { `Remove ${ file . name } ` }
160202 >
161203 ×
162204 </ button >
163205 </ span >
164206 ) ) }
165207 </ div >
166208 ) }
167- < p className = "text-xs text-gray-500 dark:text-gray-400" > Press Cmd/Ctrl + Enter to post</ p >
168- </ div >
209+ < p id = { editorHelpId } className = "text-xs text-gray-500 dark:text-gray-400" >
210+ Press Cmd/Ctrl + Enter to post
211+ </ p >
212+ </ form >
169213 </ div >
170214 ) ;
171215}
0 commit comments