1- import { useEffect , useMemo , useCallback } from 'react' ;
1+ import { useEffect , useMemo , useCallback , useState } from 'react' ;
22import React , { Children } from 'react' ;
33import ReactMarkdown , { Components } from 'react-markdown' ;
44import remarkGfm from 'remark-gfm' ;
@@ -164,37 +164,37 @@ const processMarkdownContent = (content: string): string => {
164164 '<del class="line-through text-gray-500">$1</del>' ,
165165 ) ;
166166
167- // Collapsible sections
167+ // Collapsible sections - ensure proper block separation
168168 processed = processed . replace (
169169 / : : : d e t a i l s \s + ( .* ?) \n ( [ \s \S ] * ?) : : : / gim,
170- '<details class="my-4 border border-gray-200 rounded-lg overflow-hidden bg-white">' +
171- '<summary class="bg-gray-50 px-4 py-3 cursor-pointer font-medium text-gray-800 hover:bg-gray-100 transition-colors border-b border-gray-200">$1</summary>' +
172- '<div class="px-4 py-3 text-gray-700">$2</div></details>' ,
170+ '\n\n <details class="my-4 border border-gray-200 rounded-lg overflow-hidden bg-white">' +
171+ '<summary class="bg-gray-50 px-4 py-3 cursor-pointer font-medium text-gray-800 hover:bg-gray-100 transition-colors border-b border-gray-200">$1</summary>' +
172+ '<div class="px-4 py-3 text-gray-700">$2</div></details>\n\n ' ,
173173 ) ;
174174
175- // GitHub-style alerts
175+ // GitHub-style alerts - ensure proper block separation
176176 processed = processed . replace (
177177 / : : : ( \w + ) \s * ( .* ?) \n ( [ \s \S ] * ?) : : : / gim,
178178 ( _ , type , title , content ) => {
179179 const alert =
180180 ALERT_TYPES [ type as keyof typeof ALERT_TYPES ] || ALERT_TYPES . note ;
181- return `<div class="my-4 p-4 border-l-4 ${ alert . border } ${ alert . bg } rounded-r-lg">
181+ return `\n\n <div class="my-4 p-4 border-l-4 ${ alert . border } ${ alert . bg } rounded-r-lg">
182182 <div class="flex items-center mb-2">
183183 <span class="mr-2 text-lg">${ alert . icon } </span>
184184 <strong class="${ alert . text } font-semibold uppercase text-sm tracking-wide">${ type } ${ title ? `: ${ title } ` : '' } </strong>
185185 </div>
186186 <div class="${ alert . text } ">${ content } </div>
187- </div>` ;
187+ </div>\n\n ` ;
188188 } ,
189189 ) ;
190190
191- // YouTube embeds
191+ // YouTube embeds - ensure proper block separation
192192 processed = processed . replace (
193193 / \[ y o u t u b e : \s * ( [ \w - ] + ) \] / gim,
194194 ( _ , videoId ) =>
195- `<div class="my-8 mx-auto max-w-4xl"><div class="relative rounded-xl shadow-lg overflow-hidden bg-black" style="aspect-ratio: 16/9;">
195+ `\n\n <div class="my-8 mx-auto max-w-4xl"><div class="relative rounded-xl shadow-lg overflow-hidden bg-black" style="aspect-ratio: 16/9;">
196196 <iframe src="https://www.youtube.com/embed/${ videoId } ?autoplay=0&rel=0&modestbranding=1" class="absolute inset-0 w-full h-full border-0"
197- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div>` ,
197+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" title="YouTube video player"></iframe></div></div>\n\n ` ,
198198 ) ;
199199
200200 return processed ;
@@ -205,6 +205,12 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
205205 setZoomableImages,
206206 frontmatter,
207207} ) => {
208+ const [ isClient , setIsClient ] = useState ( false ) ;
209+
210+ useEffect ( ( ) => {
211+ setIsClient ( true ) ;
212+ } , [ ] ) ;
213+
208214 const processedContent = useMemo (
209215 ( ) => processMarkdownContent ( content ) ,
210216 [ content ] ,
@@ -321,19 +327,19 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
321327
322328 const createHeading =
323329 ( level : keyof typeof headingClasses ) =>
324- ( {
325- children,
326- ...props
327- } : React . HTMLAttributes < HTMLHeadingElement > & {
328- children ?: React . ReactNode ;
329- } ) => {
330- const Tag = level ;
331- return (
332- < Tag { ...props } className = { headingClasses [ level ] } >
333- { children }
334- </ Tag >
335- ) ;
336- } ;
330+ ( {
331+ children,
332+ ...props
333+ } : React . HTMLAttributes < HTMLHeadingElement > & {
334+ children ?: React . ReactNode ;
335+ } ) => {
336+ const Tag = level ;
337+ return (
338+ < Tag { ...props } className = { headingClasses [ level ] } >
339+ { children }
340+ </ Tag >
341+ ) ;
342+ } ;
337343
338344 const components : Components = {
339345 h1 : createHeading ( 'h1' ) ,
@@ -343,14 +349,55 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
343349 h5 : createHeading ( 'h5' ) ,
344350 h6 : createHeading ( 'h6' ) ,
345351
346- p : ( { children, ...props } ) => (
347- < p
348- { ...props }
349- className = "my-4 text-gray-700 dark:text-gray-300 leading-relaxed"
350- >
351- { children }
352- </ p >
353- ) ,
352+ p : ( { children, ...props } ) => {
353+ // Convert children to array and check for problematic content
354+ const childArray = React . Children . toArray ( children ) ;
355+
356+ // Check if any child contains HTML that would create block elements or is an image
357+ const hasProblematicContent = childArray . some ( child => {
358+ if ( typeof child === 'string' ) {
359+ // Check for HTML tags that create block elements
360+ return / < ( d i v | f i g u r e | b l o c k q u o t e | p r e | t a b l e | u l | o l | d e t a i l s | i f r a m e | h [ 1 - 6 ] | i m g ) / i. test ( child ) ;
361+ }
362+ if ( React . isValidElement ( child ) ) {
363+ const type = child . type ;
364+ // Check for React components that render as block elements or images
365+ return typeof type === 'string' &&
366+ [ 'div' , 'figure' , 'blockquote' , 'pre' , 'table' , 'ul' , 'ol' , 'details' , 'iframe' , 'img' , 'span' ] . includes ( type ) ;
367+ }
368+ return false ;
369+ } ) ;
370+
371+ // Check if this paragraph only contains an image
372+ const isImageOnly = childArray . length === 1 &&
373+ React . isValidElement ( childArray [ 0 ] ) &&
374+ ( childArray [ 0 ] . type === 'img' ||
375+ ( typeof childArray [ 0 ] . type === 'function' &&
376+ childArray [ 0 ] . props && typeof childArray [ 0 ] . props === 'object' &&
377+ 'src' in childArray [ 0 ] . props ) ) ;
378+
379+ // If contains problematic content or is image-only, render as div
380+ if ( hasProblematicContent || isImageOnly ) {
381+ return (
382+ < div
383+ { ...props }
384+ className = "my-4 text-gray-700 dark:text-gray-300 leading-relaxed"
385+ >
386+ { children }
387+ </ div >
388+ ) ;
389+ }
390+
391+ // Safe to render as paragraph
392+ return (
393+ < p
394+ { ...props }
395+ className = "my-4 text-gray-700 dark:text-gray-300 leading-relaxed"
396+ >
397+ { children }
398+ </ p >
399+ ) ;
400+ } ,
354401
355402 blockquote : ( { children, ...props } ) => (
356403 < blockquote
@@ -446,28 +493,30 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
446493 const imageSrc =
447494 src === '' && frontmatter ?. image ? String ( frontmatter . image ) : src ;
448495 return (
449- < figure className = "flex flex-col items-center my-6" >
450- < div className = "overflow-hidden rounded-lg shadow-md hover:shadow-lg transition-all duration-300 border border-gray-200" >
451- < img
452- { ...props }
453- src = { imageSrc }
454- alt = { alt }
455- title = { title || alt || '' }
456- className = "max-w-full h-auto rounded-lg object-contain hover:scale-105 transition-transform duration-300"
457- data-zoomable = "true"
458- loading = "lazy"
459- onError = { ( e ) => {
460- ( e . target as HTMLImageElement ) . src =
461- '/assets/Images/SugarNewsLogo.webp' ;
462- } }
463- />
464- </ div >
465- { ( title || alt ) && (
466- < figcaption className = "text-center text-sm text-gray-600 mt-3 italic" >
467- { title || alt }
468- </ figcaption >
469- ) }
470- </ figure >
496+ < span className = "block my-6" >
497+ < span className = "flex flex-col items-center" >
498+ < span className = "overflow-hidden rounded-lg shadow-md hover:shadow-lg transition-all duration-300 border border-gray-200 inline-block" >
499+ < img
500+ { ...props }
501+ src = { imageSrc }
502+ alt = { alt }
503+ title = { title || alt || '' }
504+ className = "max-w-full h-auto rounded-lg object-contain hover:scale-105 transition-transform duration-300"
505+ data-zoomable = "true"
506+ loading = "lazy"
507+ onError = { ( e ) => {
508+ ( e . target as HTMLImageElement ) . src =
509+ '/assets/Images/SugarNewsLogo.webp' ;
510+ } }
511+ />
512+ </ span >
513+ { ( title || alt ) && (
514+ < span className = "text-center text-sm text-gray-600 mt-3 italic block" >
515+ { title || alt }
516+ </ span >
517+ ) }
518+ </ span >
519+ </ span >
471520 ) ;
472521 } ,
473522
@@ -624,8 +673,23 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
624673 { children }
625674 </ summary >
626675 ) ,
676+
677+
627678 } ;
628679
680+ // Prevent hydration mismatch by only rendering on client
681+ if ( ! isClient ) {
682+ return (
683+ < div className = "prose prose-lg dark:prose-invert prose-headings:dark:text-gray-100 prose-p:dark:text-gray-300 prose-strong:dark:text-gray-100 prose-em:dark:text-gray-300 prose-li:dark:text-gray-300 max-w-none" >
684+ < div className = "animate-pulse" >
685+ < div className = "h-4 bg-gray-200 rounded w-3/4 mb-4" > </ div >
686+ < div className = "h-4 bg-gray-200 rounded w-1/2 mb-4" > </ div >
687+ < div className = "h-4 bg-gray-200 rounded w-5/6 mb-4" > </ div >
688+ </ div >
689+ </ div >
690+ ) ;
691+ }
692+
629693 return (
630694 < div className = "prose prose-lg dark:prose-invert prose-headings:dark:text-gray-100 prose-p:dark:text-gray-300 prose-strong:dark:text-gray-100 prose-em:dark:text-gray-300 prose-li:dark:text-gray-300 max-w-none" >
631695 < ReactMarkdown
0 commit comments