|
|
|
|
@@ -295,10 +295,12 @@ const SectionImageBlock = ({
|
|
|
|
|
image,
|
|
|
|
|
loading,
|
|
|
|
|
heading,
|
|
|
|
|
showPrompt = true,
|
|
|
|
|
}: {
|
|
|
|
|
image: ImageRecord | null;
|
|
|
|
|
loading: boolean;
|
|
|
|
|
heading: string;
|
|
|
|
|
showPrompt?: boolean;
|
|
|
|
|
}) => {
|
|
|
|
|
if (!image && !loading) return null;
|
|
|
|
|
|
|
|
|
|
@@ -323,7 +325,7 @@ const SectionImageBlock = ({
|
|
|
|
|
<ImageStatusPill status={image?.status} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{image?.caption && (
|
|
|
|
|
{showPrompt && image?.caption && (
|
|
|
|
|
<figcaption className="space-y-3 px-6 py-5 text-sm leading-relaxed text-gray-600 dark:text-gray-300">
|
|
|
|
|
<p className="font-semibold uppercase tracking-[0.25em] text-gray-400 dark:text-gray-500">
|
|
|
|
|
Image Caption
|
|
|
|
|
@@ -391,12 +393,14 @@ const splitAtFirstH3 = (html: string): { beforeH3: string; h3AndAfter: string }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ContentSectionBlock - Renders a content section with image layout based on aspect ratio
|
|
|
|
|
* ContentSectionBlock - Renders a content section with image layout based on distribution pattern
|
|
|
|
|
*
|
|
|
|
|
* Layout rules:
|
|
|
|
|
* - Single landscape image: 100% width (full width)
|
|
|
|
|
* - Single square image: 50% width (centered)
|
|
|
|
|
* - Two square images (paired): Side by side (50% each)
|
|
|
|
|
* Layout rules (first 4 sections):
|
|
|
|
|
* - Section 1: Square image right-aligned (50%) with description
|
|
|
|
|
* - Section 2: Landscape image full-width (1024px) with description
|
|
|
|
|
* - Section 3: Square image left-aligned (50%) with description
|
|
|
|
|
* - Section 4: Landscape image full-width (1024px) with description
|
|
|
|
|
* - Sections 5+: Reuse images without descriptions
|
|
|
|
|
*/
|
|
|
|
|
const ContentSectionBlock = ({
|
|
|
|
|
section,
|
|
|
|
|
@@ -404,32 +408,33 @@ const ContentSectionBlock = ({
|
|
|
|
|
loading,
|
|
|
|
|
index,
|
|
|
|
|
aspectRatio = 'square',
|
|
|
|
|
pairedSquareImage = null,
|
|
|
|
|
imageAlign = 'full',
|
|
|
|
|
showDescription = true,
|
|
|
|
|
}: {
|
|
|
|
|
section: ArticleSection;
|
|
|
|
|
image: ImageRecord | null;
|
|
|
|
|
loading: boolean;
|
|
|
|
|
index: number;
|
|
|
|
|
aspectRatio?: 'square' | 'landscape';
|
|
|
|
|
pairedSquareImage?: ImageRecord | null;
|
|
|
|
|
imageAlign?: 'left' | 'right' | 'full';
|
|
|
|
|
showDescription?: boolean;
|
|
|
|
|
}) => {
|
|
|
|
|
const hasImage = Boolean(image);
|
|
|
|
|
const hasPairedImage = Boolean(pairedSquareImage);
|
|
|
|
|
const headingLabel = section.heading || `Section ${index + 1}`;
|
|
|
|
|
const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml);
|
|
|
|
|
|
|
|
|
|
// Determine image container width class based on aspect ratio and pairing
|
|
|
|
|
// Determine image container width class based on aspect ratio and alignment
|
|
|
|
|
const getImageContainerClass = () => {
|
|
|
|
|
if (hasPairedImage) {
|
|
|
|
|
// Two squares side by side
|
|
|
|
|
return 'w-full';
|
|
|
|
|
}
|
|
|
|
|
if (aspectRatio === 'landscape') {
|
|
|
|
|
// Landscape: 100% width
|
|
|
|
|
return 'w-full';
|
|
|
|
|
return 'w-full max-w-[1024px] mx-auto';
|
|
|
|
|
}
|
|
|
|
|
// Single square: 50% width centered
|
|
|
|
|
return 'w-full max-w-[50%]';
|
|
|
|
|
if (imageAlign === 'left') {
|
|
|
|
|
return 'w-full max-w-[50%] mr-auto';
|
|
|
|
|
}
|
|
|
|
|
if (imageAlign === 'right') {
|
|
|
|
|
return 'w-full max-w-[50%] ml-auto';
|
|
|
|
|
}
|
|
|
|
|
return 'w-full max-w-[50%] mx-auto';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@@ -460,25 +465,12 @@ const ContentSectionBlock = ({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Image section - layout depends on aspect ratio */}
|
|
|
|
|
{/* Image section - layout depends on aspect ratio and alignment */}
|
|
|
|
|
{hasImage && (
|
|
|
|
|
<div className="flex justify-center">
|
|
|
|
|
{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 className={getImageContainerClass()}>
|
|
|
|
|
<SectionImageBlock image={image} loading={loading} heading={headingLabel} showPrompt={showDescription} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
@@ -513,21 +505,22 @@ interface ArticleBodyProps {
|
|
|
|
|
const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => {
|
|
|
|
|
const hasStructuredSections = sections.length > 0;
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
};
|
|
|
|
|
// Image distribution mapping for first 4 sections (matches WordPress template)
|
|
|
|
|
const imageDistribution = [
|
|
|
|
|
{ position: 0, type: 'square' as const, align: 'right' as const }, // Section 1
|
|
|
|
|
{ position: 3, type: 'landscape' as const, align: 'full' as const }, // Section 2
|
|
|
|
|
{ position: 2, type: 'square' as const, align: 'left' as const }, // Section 3
|
|
|
|
|
{ position: 1, type: 'landscape' as const, align: 'full' as const }, // Section 4
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
// Reuse pattern for sections 5+ (without descriptions)
|
|
|
|
|
const reusePattern = [1, 0, 3, 2];
|
|
|
|
|
|
|
|
|
|
// Get image aspect ratio from record or fallback to position-based calculation
|
|
|
|
|
const getImageAspectRatio = (image: ImageRecord | null, position: number): 'square' | 'landscape' => {
|
|
|
|
|
if (image?.aspect_ratio) return image.aspect_ratio;
|
|
|
|
|
// Fallback: positions 0, 2 are square, positions 1, 3 are landscape
|
|
|
|
|
return position === 0 || position === 2 ? 'square' : 'landscape';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!hasStructuredSections && !introHtml && rawHtml) {
|
|
|
|
|
@@ -540,42 +533,46 @@ 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) => {
|
|
|
|
|
// Skip if this image was already rendered as part of a pair
|
|
|
|
|
if (renderedPairIndices.has(index)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
{sections.map((section, sectionIndex) => {
|
|
|
|
|
let image: ImageRecord | null = null;
|
|
|
|
|
let aspectRatio: 'square' | 'landscape' = 'landscape';
|
|
|
|
|
let imageAlign: 'left' | 'right' | 'full' = 'full';
|
|
|
|
|
let showDescription = true;
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
// First 4 sections: use distribution pattern
|
|
|
|
|
if (sectionIndex < 4) {
|
|
|
|
|
const dist = imageDistribution[sectionIndex];
|
|
|
|
|
const imgPosition = dist.position;
|
|
|
|
|
image = sectionImages[imgPosition] ?? null;
|
|
|
|
|
aspectRatio = dist.type;
|
|
|
|
|
imageAlign = dist.align;
|
|
|
|
|
showDescription = true;
|
|
|
|
|
}
|
|
|
|
|
// Sections 5+: reuse images without descriptions
|
|
|
|
|
else {
|
|
|
|
|
const reuseIndex = (sectionIndex - 4) % reusePattern.length;
|
|
|
|
|
const imgPosition = reusePattern[reuseIndex];
|
|
|
|
|
image = sectionImages[imgPosition] ?? null;
|
|
|
|
|
if (image) {
|
|
|
|
|
aspectRatio = getImageAspectRatio(image, imgPosition);
|
|
|
|
|
imageAlign = (aspectRatio === 'square') ? (reuseIndex % 2 === 0 ? 'right' : 'left') : 'full';
|
|
|
|
|
}
|
|
|
|
|
showDescription = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ContentSectionBlock
|
|
|
|
|
key={section.id || `section-${index}`}
|
|
|
|
|
key={section.id || `section-${sectionIndex}`}
|
|
|
|
|
section={section}
|
|
|
|
|
image={currentImage}
|
|
|
|
|
image={image}
|
|
|
|
|
loading={imagesLoading}
|
|
|
|
|
index={index}
|
|
|
|
|
aspectRatio={currentAspectRatio}
|
|
|
|
|
pairedSquareImage={pairedSquareImage}
|
|
|
|
|
index={sectionIndex}
|
|
|
|
|
aspectRatio={aspectRatio}
|
|
|
|
|
imageAlign={imageAlign}
|
|
|
|
|
showDescription={showDescription}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
@@ -749,7 +746,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
|
|
|
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="animate-pulse">
|
|
|
|
|
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-6"></div>
|
|
|
|
|
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded w-3/4 mb-4"></div>
|
|
|
|
|
@@ -766,7 +763,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|
|
|
|
if (!content) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
|
|
|
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-8 text-center">
|
|
|
|
|
<XCircleIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
|
|
|
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
|
|
|
|
|
@@ -834,7 +831,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
|
|
|
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
|
{/* Back Button */}
|
|
|
|
|
{onBack && (
|
|
|
|
|
<Button
|
|
|
|
|
@@ -1103,7 +1100,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|
|
|
|
|
|
|
|
|
{/* Featured Image */}
|
|
|
|
|
{shouldShowFeaturedBlock && (
|
|
|
|
|
<div className="mb-12 max-w-[800px] mx-auto">
|
|
|
|
|
<div className="mb-12 max-w-[1024px] mx-auto">
|
|
|
|
|
<FeaturedImageBlock image={resolvedFeaturedImage} loading={imagesLoading} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|