content view template final version

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-25 04:06:19 +00:00
parent 826ad89a3e
commit b0c14ccc32
2 changed files with 159 additions and 19 deletions

View File

@@ -345,19 +345,76 @@ const IntroBlock = ({ html }: { html: string }) => (
</section>
);
// Helper to split content at first H3 tag
const splitAtFirstH3 = (html: string): { beforeH3: string; h3AndAfter: string } => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const h3 = doc.querySelector('h3');
if (!h3) {
return { beforeH3: html, h3AndAfter: '' };
}
const beforeNodes: Node[] = [];
const afterNodes: Node[] = [];
let foundH3 = false;
Array.from(doc.body.childNodes).forEach((node) => {
if (node === h3) {
foundH3 = true;
afterNodes.push(node);
} else if (foundH3) {
afterNodes.push(node);
} else {
beforeNodes.push(node);
}
});
const serializeNodes = (nodes: Node[]): string =>
nodes
.map((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
return (node as HTMLElement).outerHTML;
}
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent ?? '';
}
return '';
})
.join('');
return {
beforeH3: serializeNodes(beforeNodes),
h3AndAfter: serializeNodes(afterNodes),
};
};
// 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;
};
const ContentSectionBlock = ({
section,
image,
loading,
index,
imagePlacement = 'right',
firstImage = null,
}: {
section: ArticleSection;
image: ImageRecord | null;
loading: boolean;
index: number;
imagePlacement?: 'left' | 'center' | 'right';
firstImage?: ImageRecord | null;
}) => {
const hasImage = Boolean(image);
const headingLabel = section.heading || `Section ${index + 1}`;
const sectionHasTable = hasTable(section.bodyHtml);
const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml);
return (
<section id={section.id} className="group/section scroll-mt-24">
@@ -377,16 +434,86 @@ const ContentSectionBlock = ({
</div>
</div>
<div className={hasImage ? 'grid gap-10 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]' : ''}>
<div className={`content-html prose prose-lg max-w-none text-gray-800 dark:prose-invert ${hasImage ? '' : ''}`}>
<div dangerouslySetInnerHTML={{ __html: section.bodyHtml }} />
</div>
{hasImage && (
<div className="flex flex-col gap-4">
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
{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 */}
<div className="flex justify-center">
<div className="w-full max-w-[60%]">
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
</div>
</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>
</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`}>
<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>
</section>
@@ -404,6 +531,14 @@ 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';
};
if (!hasStructuredSections && !introHtml && rawHtml) {
return (
<div className="overflow-hidden rounded-3xl border border-slate-200/80 bg-white/90 p-8 shadow-lg shadow-slate-200/50 dark:border-gray-800/70 dark:bg-gray-900/70 dark:shadow-black/20">
@@ -414,6 +549,9 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm
);
}
// Get the first in-article image (position 0)
const firstImage = sectionImages.length > 0 ? sectionImages[0] : null;
return (
<div className="space-y-12">
{introHtml && <IntroBlock html={introHtml} />}
@@ -424,6 +562,8 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm
image={sectionImages[index] ?? null}
loading={imagesLoading}
index={index}
imagePlacement={getImagePlacement(index)}
firstImage={firstImage}
/>
))}
</div>
@@ -535,13 +675,13 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
const byPosition = new Map<number, ImageRecord>();
sorted.forEach((img, index) => {
const pos = img.position ?? index + 1;
const pos = img.position ?? index;
byPosition.set(pos, img);
});
const usedPositions = new Set<number>();
const merged: ImageRecord[] = prompts.map((prompt, index) => {
const position = index + 1;
const position = index; // 0-based position matching section array index
const existing = byPosition.get(position);
usedPositions.add(position);
if (existing) {
@@ -561,7 +701,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
image_path: undefined,
prompt,
status: 'pending',
position,
position, // 0-based position
created_at: '',
updated_at: '',
account_id: undefined,
@@ -569,7 +709,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
});
sorted.forEach((img, idx) => {
const position = img.position ?? idx + 1;
const position = img.position ?? idx;
if (!usedPositions.has(position)) {
merged.push(img);
}
@@ -596,7 +736,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-[1200px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-[1440px] 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>
@@ -613,7 +753,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-[1200px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-[1440px] 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>
@@ -663,7 +803,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-[1200px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
{/* Back Button */}
{onBack && (
<button
@@ -676,7 +816,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
)}
{/* Main Content Card */}
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-white dark:bg-gray-800 overflow-hidden">
{/* Header Section */}
<div className="bg-gradient-to-r from-brand-500 to-brand-600 px-8 py-6 text-white">
<div className="flex items-start justify-between gap-4">