IMage genartion service and models revamp - #Migration Runs
This commit is contained in:
@@ -390,37 +390,53 @@ const splitAtFirstH3 = (html: string): { beforeH3: string; h3AndAfter: string }
|
||||
};
|
||||
};
|
||||
|
||||
// Helper to check if section contains a table
|
||||
const hasTable = (html: string): boolean => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
return doc.querySelector('table') !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* ContentSectionBlock - Renders a content section with image layout based on aspect ratio
|
||||
*
|
||||
* Layout rules:
|
||||
* - Single landscape image: 100% width (full width)
|
||||
* - Single square image: 50% width (centered)
|
||||
* - Two square images (paired): Side by side (50% each)
|
||||
*/
|
||||
const ContentSectionBlock = ({
|
||||
section,
|
||||
image,
|
||||
loading,
|
||||
index,
|
||||
imagePlacement = 'right',
|
||||
firstImage = null,
|
||||
aspectRatio = 'square',
|
||||
pairedSquareImage = null,
|
||||
}: {
|
||||
section: ArticleSection;
|
||||
image: ImageRecord | null;
|
||||
loading: boolean;
|
||||
index: number;
|
||||
imagePlacement?: 'left' | 'center' | 'right';
|
||||
firstImage?: ImageRecord | null;
|
||||
aspectRatio?: 'square' | 'landscape';
|
||||
pairedSquareImage?: ImageRecord | null;
|
||||
}) => {
|
||||
const hasImage = Boolean(image);
|
||||
const hasPairedImage = Boolean(pairedSquareImage);
|
||||
const headingLabel = section.heading || `Section ${index + 1}`;
|
||||
const sectionHasTable = hasTable(section.bodyHtml);
|
||||
const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml);
|
||||
|
||||
// Determine image container width class based on aspect ratio and pairing
|
||||
const getImageContainerClass = () => {
|
||||
if (hasPairedImage) {
|
||||
// Two squares side by side
|
||||
return 'w-full';
|
||||
}
|
||||
if (aspectRatio === 'landscape') {
|
||||
// Landscape: 100% width
|
||||
return 'w-full';
|
||||
}
|
||||
// Single square: 50% width centered
|
||||
return 'w-full max-w-[50%]';
|
||||
};
|
||||
|
||||
return (
|
||||
<section id={section.id} className="group/section scroll-mt-24">
|
||||
<div className="overflow-hidden rounded-3xl border border-gray-200/80 bg-white/90 shadow-lg shadow-slate-200/50 backdrop-blur-sm transition-transform duration-300 group-hover/section:-translate-y-1 dark:border-gray-800/70 dark:bg-gray-900/70 dark:shadow-black/20">
|
||||
<div className="flex flex-col gap-6 p-8 sm:p-10">
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-brand-500/10 text-sm font-semibold text-brand-600 dark:bg-brand-500/20 dark:text-brand-300">
|
||||
{index + 1}
|
||||
@@ -435,86 +451,51 @@ const ContentSectionBlock = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{imagePlacement === 'center' && hasImage ? (
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Content before H3 */}
|
||||
{beforeH3 && (
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: beforeH3 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Centered image before H3 */}
|
||||
{/* Content layout with images */}
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Content before H3 */}
|
||||
{beforeH3 && (
|
||||
<div className="content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert">
|
||||
<div dangerouslySetInnerHTML={{ __html: beforeH3 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image section - layout depends on aspect ratio */}
|
||||
{hasImage && (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-[60%]">
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
||||
</div>
|
||||
{hasPairedImage ? (
|
||||
// Two squares side by side (50% each)
|
||||
<div className="grid w-full grid-cols-2 gap-6">
|
||||
<div className="w-full">
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<SectionImageBlock image={pairedSquareImage} loading={loading} heading={`${headingLabel} (2)`} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Single image with width based on aspect ratio
|
||||
<div className={getImageContainerClass()}>
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* H3 and remaining content */}
|
||||
{h3AndAfter && (
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: h3AndAfter }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback if no H3 found */}
|
||||
{!beforeH3 && !h3AndAfter && (
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: section.bodyHtml }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : sectionHasTable && hasImage && firstImage ? (
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Content before H3 */}
|
||||
{beforeH3 && (
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: beforeH3 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Two images side by side at 50% width each */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="w-full">
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<SectionImageBlock image={firstImage} loading={loading} heading="First Article Image" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* H3 and remaining content */}
|
||||
{h3AndAfter && (
|
||||
<div className="content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert">
|
||||
<div dangerouslySetInnerHTML={{ __html: h3AndAfter }} />
|
||||
</div>
|
||||
|
||||
{/* H3 and remaining content */}
|
||||
{h3AndAfter && (
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: h3AndAfter }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback if no H3 found */}
|
||||
{!beforeH3 && !h3AndAfter && (
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: section.bodyHtml }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={hasImage ? `grid gap-10 ${imagePlacement === 'left' ? 'lg:grid-cols-[minmax(0,40%)_minmax(0,60%)]' : 'lg:grid-cols-[minmax(0,60%)_minmax(0,40%)]'}` : ''}>
|
||||
{imagePlacement === 'left' && hasImage && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
||||
</div>
|
||||
)}
|
||||
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert`}>
|
||||
)}
|
||||
|
||||
{/* Fallback if no H3 structure found */}
|
||||
{!beforeH3 && !h3AndAfter && (
|
||||
<div className="content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert">
|
||||
<div dangerouslySetInnerHTML={{ __html: section.bodyHtml }} />
|
||||
</div>
|
||||
{imagePlacement === 'right' && hasImage && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -532,12 +513,21 @@ interface ArticleBodyProps {
|
||||
const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => {
|
||||
const hasStructuredSections = sections.length > 0;
|
||||
|
||||
// Calculate image placement: right → center → left → repeat
|
||||
const getImagePlacement = (index: number): 'left' | 'center' | 'right' => {
|
||||
const position = index % 3;
|
||||
if (position === 0) return 'right';
|
||||
if (position === 1) return 'center';
|
||||
return 'left';
|
||||
// Determine image aspect ratio from record or fallback to position-based calculation
|
||||
// Position 0, 2 = square (1024x1024), Position 1, 3 = landscape (model-specific)
|
||||
const getImageAspectRatio = (image: ImageRecord | null, index: number): 'square' | 'landscape' => {
|
||||
if (image?.aspect_ratio) return image.aspect_ratio;
|
||||
// Fallback: even positions (0, 2) are square, odd positions (1, 3) are landscape
|
||||
return index % 2 === 0 ? 'square' : 'landscape';
|
||||
};
|
||||
|
||||
// Check if two consecutive images are both squares (for side-by-side layout)
|
||||
const getNextSquareImage = (currentIndex: number): ImageRecord | null => {
|
||||
const nextImage = sectionImages[currentIndex + 1];
|
||||
if (nextImage && getImageAspectRatio(nextImage, currentIndex + 1) === 'square') {
|
||||
return nextImage;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!hasStructuredSections && !introHtml && rawHtml) {
|
||||
@@ -553,20 +543,42 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm
|
||||
// Get the first in-article image (position 0)
|
||||
const firstImage = sectionImages.length > 0 ? sectionImages[0] : null;
|
||||
|
||||
// Track which images have been rendered as pairs (to skip the second in the pair)
|
||||
const renderedPairIndices = new Set<number>();
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{introHtml && <IntroBlock html={introHtml} />}
|
||||
{sections.map((section, index) => (
|
||||
<ContentSectionBlock
|
||||
key={section.id || `section-${index}`}
|
||||
section={section}
|
||||
image={sectionImages[index] ?? null}
|
||||
loading={imagesLoading}
|
||||
index={index}
|
||||
imagePlacement={getImagePlacement(index)}
|
||||
firstImage={firstImage}
|
||||
/>
|
||||
))}
|
||||
{sections.map((section, index) => {
|
||||
// Skip if this image was already rendered as part of a pair
|
||||
if (renderedPairIndices.has(index)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentImage = sectionImages[index] ?? null;
|
||||
const currentAspectRatio = getImageAspectRatio(currentImage, index);
|
||||
|
||||
// Check if current is square and next is also square for side-by-side layout
|
||||
let pairedSquareImage: ImageRecord | null = null;
|
||||
if (currentAspectRatio === 'square') {
|
||||
pairedSquareImage = getNextSquareImage(index);
|
||||
if (pairedSquareImage) {
|
||||
renderedPairIndices.add(index + 1); // Mark next as rendered
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentSectionBlock
|
||||
key={section.id || `section-${index}`}
|
||||
section={section}
|
||||
image={currentImage}
|
||||
loading={imagesLoading}
|
||||
index={index}
|
||||
aspectRatio={currentAspectRatio}
|
||||
pairedSquareImage={pairedSquareImage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user