@@ -12,6 +12,7 @@ import {
1212 ChatBubbleLeftIcon ,
1313 PhotoIcon ,
1414 XMarkIcon ,
15+ ChevronRightIcon ,
1516} from '@heroicons/react/24/outline' ;
1617import toast from 'react-hot-toast' ;
1718import * as api from '../../services/api' ;
@@ -37,6 +38,10 @@ export function PostDetailPage() {
3738 const [ previewUrls , setPreviewUrls ] = useState < string [ ] > ( [ ] ) ;
3839 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
3940
41+ // Image modal state for post attachments
42+ const [ isImageModalOpen , setIsImageModalOpen ] = useState ( false ) ;
43+ const [ activeImageIndex , setActiveImageIndex ] = useState ( 0 ) ;
44+
4045 // Get current board to retrieve available statuses
4146 const currentBoard = currentPost ?. board ? boards . find ( b => b . id === currentPost . board . id ) : null ;
4247 const availableStatuses = ( currentBoard ?. statuses || [ 'open' , 'planned' , 'in progress' , 'complete' ] ) as string [ ] ;
@@ -269,6 +274,29 @@ export function PostDetailPage() {
269274 </ p >
270275 ) }
271276
277+ { /* Post Images */ }
278+ { currentPost . imageURLs && currentPost . imageURLs . length > 0 && (
279+ < div className = "of-mt-4 of-flex of-flex-wrap of-gap-2" >
280+ { currentPost . imageURLs . map ( ( url , index ) => (
281+ < button
282+ key = { index }
283+ type = "button"
284+ onClick = { ( ) => {
285+ setActiveImageIndex ( index ) ;
286+ setIsImageModalOpen ( true ) ;
287+ } }
288+ className = "of-block focus:of-outline-none focus:of-ring-2 focus:of-ring-primary/40 of-rounded-lg"
289+ >
290+ < img
291+ src = { url }
292+ alt = { `Attachment ${ index + 1 } ` }
293+ className = "of-max-w-[200px] of-max-h-[150px] of-object-cover of-rounded-lg of-border of-border-gray-200 hover:of-opacity-90 of-transition-opacity"
294+ />
295+ </ button >
296+ ) ) }
297+ </ div >
298+ ) }
299+
272300 { /* Meta */ }
273301 < div className = "of-mt-4 of-flex of-items-center of-gap-4 of-text-sm of-text-gray-500" >
274302 < div className = "of-flex of-items-center of-gap-1" >
@@ -478,6 +506,99 @@ export function PostDetailPage() {
478506 </ div >
479507 ) }
480508 </ div >
509+
510+ { /* Post image modal */ }
511+ < PostImageModal
512+ isOpen = { isImageModalOpen }
513+ onClose = { ( ) => setIsImageModalOpen ( false ) }
514+ imageURLs = { currentPost . imageURLs || [ ] }
515+ activeIndex = { activeImageIndex }
516+ setActiveIndex = { setActiveImageIndex }
517+ />
518+ </ div >
519+ ) ;
520+ }
521+
522+ // Image modal for post attachments
523+ function PostImageModal ( {
524+ isOpen,
525+ onClose,
526+ imageURLs,
527+ activeIndex,
528+ setActiveIndex,
529+ } : {
530+ isOpen : boolean ;
531+ onClose : ( ) => void ;
532+ imageURLs : string [ ] ;
533+ activeIndex : number ;
534+ setActiveIndex : ( index : number ) => void ;
535+ } ) {
536+ if ( ! isOpen || ! imageURLs || imageURLs . length === 0 ) return null ;
537+
538+ const total = imageURLs . length ;
539+
540+ const goPrev = ( ) => {
541+ setActiveIndex ( ( activeIndex - 1 + total ) % total ) ;
542+ } ;
543+
544+ const goNext = ( ) => {
545+ setActiveIndex ( ( activeIndex + 1 ) % total ) ;
546+ } ;
547+
548+ return (
549+ < div
550+ className = "of-fixed of-inset-0 of-z-50 of-bg-black/70 of-flex of-items-center of-justify-center"
551+ onClick = { onClose }
552+ >
553+ < div
554+ className = "of-relative of-max-w-4xl of-w-full of-mx-4 of-bg-transparent"
555+ onClick = { ( e ) => e . stopPropagation ( ) }
556+ >
557+ { /* Close button */ }
558+ < button
559+ type = "button"
560+ onClick = { onClose }
561+ className = "of-absolute of-top-0 of-right-0 of-m-2 of-inline-flex of-items-center of-justify-center of-rounded-full of-bg-black/60 of-text-white of-p-2 hover:of-bg-black/80"
562+ aria-label = "Close image viewer"
563+ >
564+ < XMarkIcon className = "of-w-5 of-h-5" />
565+ </ button >
566+
567+ { /* Image */ }
568+ < div className = "of-flex of-items-center of-justify-center of-bg-black/40 of-rounded-lg of-overflow-hidden of-p-2" >
569+ < img
570+ src = { imageURLs [ activeIndex ] }
571+ alt = { `Attachment ${ activeIndex + 1 } ` }
572+ className = "of-max-h-[80vh] of-w-auto of-rounded-lg of-object-contain"
573+ />
574+ </ div >
575+
576+ { /* Navigation */ }
577+ { total > 1 && (
578+ < >
579+ < button
580+ type = "button"
581+ onClick = { goPrev }
582+ className = "of-absolute of-left-0 of-top-1/2 of--translate-y-1/2 of-ml-2 of-inline-flex of-items-center of-justify-center of-rounded-full of-bg-black/60 of-text-white of-p-2 hover:of-bg-black/80"
583+ aria-label = "Previous image"
584+ >
585+ < ChevronLeftIcon className = "of-w-5 of-h-5" />
586+ </ button >
587+ < button
588+ type = "button"
589+ onClick = { goNext }
590+ className = "of-absolute of-right-0 of-top-1/2 of--translate-y-1/2 of-mr-2 of-inline-flex of-items-center of-justify-center of-rounded-full of-bg-black/60 of-text-white of-p-2 hover:of-bg-black/80"
591+ aria-label = "Next image"
592+ >
593+ < ChevronRightIcon className = "of-w-5 of-h-5" />
594+ </ button >
595+
596+ < div className = "of-absolute of-bottom-0 of-left-1/2 of--translate-x-1/2 of-mb-3 of-px-3 of-py-1 of-rounded-full of-bg-black/60 of-text-xs of-text-white" >
597+ { activeIndex + 1 } / { total }
598+ </ div >
599+ </ >
600+ ) }
601+ </ div >
481602 </ div >
482603 ) ;
483604}
0 commit comments