Enhance HTMLContentRenderer and ToggleTableRow for improved content handling and metadata display
- Updated HTMLContentRenderer to directly utilize HTML from content objects. - Modified ToggleTableRow to extract and display content metadata, including primary and secondary keywords, tags, categories, and meta descriptions. - Refactored badge rendering logic for better organization and clarity in the UI. - Improved content fallback mechanisms in the Content page for better user experience.
This commit is contained in:
@@ -18,6 +18,14 @@ interface HTMLContentRendererProps {
|
|||||||
function formatContentOutline(content: any): string {
|
function formatContentOutline(content: any): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
|
// If object contains a `content` field with HTML, use that directly
|
||||||
|
if (typeof content === 'object' && content !== null && 'content' in content) {
|
||||||
|
const mainContent = (content as any).content;
|
||||||
|
if (typeof mainContent === 'string' && mainContent.trim().length > 0) {
|
||||||
|
return sanitizeHTML(mainContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let html = '<div class="content-outline">';
|
let html = '<div class="content-outline">';
|
||||||
|
|
||||||
// Handle introduction section - can be object or string
|
// Handle introduction section - can be object or string
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { ChevronDownIcon, HorizontaLDots } from '../../icons';
|
import { ChevronDownIcon, HorizontaLDots } from '../../icons';
|
||||||
import HTMLContentRenderer from './HTMLContentRenderer';
|
import HTMLContentRenderer from './HTMLContentRenderer';
|
||||||
|
import Badge from '../ui/badge/Badge';
|
||||||
|
|
||||||
interface ToggleTableRowProps {
|
interface ToggleTableRowProps {
|
||||||
/** The row data */
|
/** The row data */
|
||||||
@@ -44,6 +45,13 @@ const ToggleTableRow: React.FC<ToggleTableRowProps> = ({
|
|||||||
|
|
||||||
// Get content - handle fallback to description if primary contentKey is empty
|
// Get content - handle fallback to description if primary contentKey is empty
|
||||||
let content = row[contentKey];
|
let content = row[contentKey];
|
||||||
|
let contentMetadata: Record<string, any> | null = null;
|
||||||
|
|
||||||
|
if (content && typeof content === 'object' && content !== null && 'content' in content) {
|
||||||
|
contentMetadata = { ...content };
|
||||||
|
content = content.content;
|
||||||
|
}
|
||||||
|
|
||||||
if (!content || (typeof content === 'string' && content.trim().length === 0)) {
|
if (!content || (typeof content === 'string' && content.trim().length === 0)) {
|
||||||
// Try fallback to description if primary content is empty
|
// Try fallback to description if primary content is empty
|
||||||
content = row.description || row.content_outline || null;
|
content = row.description || row.content_outline || null;
|
||||||
@@ -104,9 +112,157 @@ const ToggleTableRow: React.FC<ToggleTableRowProps> = ({
|
|||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="py-4 px-2">
|
<div className="py-4 px-2">
|
||||||
<div className="mb-2 text-xs font-semibold uppercase text-gray-500 dark:text-gray-400 tracking-wide">
|
<div className="flex flex-col gap-3">
|
||||||
{contentLabel}
|
<div className="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400 tracking-wide">
|
||||||
|
{contentLabel}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata badges */}
|
||||||
|
<ToggleMetadata row={row} contentMetadata={contentMetadata} />
|
||||||
|
|
||||||
|
{/* Rendered content */}
|
||||||
|
<div className="html-content-wrapper">
|
||||||
|
<HTMLContentRenderer
|
||||||
|
content={content}
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ToggleMetadataProps {
|
||||||
|
row: any;
|
||||||
|
contentMetadata: Record<string, any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleMetadata: React.FC<ToggleMetadataProps> = ({ row, contentMetadata }) => {
|
||||||
|
const primaryKeyword =
|
||||||
|
row.content_primary_keyword ||
|
||||||
|
row.primary_keyword ||
|
||||||
|
contentMetadata?.primary_keyword ||
|
||||||
|
contentMetadata?.metadata?.primary_keyword ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
const secondaryKeywords =
|
||||||
|
row.content_secondary_keywords ||
|
||||||
|
row.secondary_keywords ||
|
||||||
|
contentMetadata?.secondary_keywords ||
|
||||||
|
contentMetadata?.metadata?.secondary_keywords ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
const tags =
|
||||||
|
row.content_tags ||
|
||||||
|
row.tags ||
|
||||||
|
contentMetadata?.tags ||
|
||||||
|
contentMetadata?.metadata?.tags ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
const categories =
|
||||||
|
row.content_categories ||
|
||||||
|
row.categories ||
|
||||||
|
contentMetadata?.categories ||
|
||||||
|
contentMetadata?.metadata?.categories ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
const metaDescription =
|
||||||
|
row.meta_description ||
|
||||||
|
row.content_meta_description ||
|
||||||
|
contentMetadata?.meta_description ||
|
||||||
|
contentMetadata?.metadata?.meta_description ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
const hasMetadata =
|
||||||
|
primaryKeyword ||
|
||||||
|
(secondaryKeywords && secondaryKeywords.length > 0) ||
|
||||||
|
(tags && tags.length > 0) ||
|
||||||
|
(categories && categories.length > 0) ||
|
||||||
|
metaDescription;
|
||||||
|
|
||||||
|
if (!hasMetadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderBadgeList = (items: any, color: 'info' | 'light' = 'light') => {
|
||||||
|
if (!items) return null;
|
||||||
|
const list = Array.isArray(items) ? items : [items];
|
||||||
|
if (list.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<Badge
|
||||||
|
key={`${item}-${index}`}
|
||||||
|
color={color}
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{primaryKeyword && (
|
||||||
|
<div className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium text-gray-600 dark:text-gray-400">Primary Keyword:</span>
|
||||||
|
<Badge color="info" size="sm" variant="light">
|
||||||
|
{primaryKeyword}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const badges = renderBadgeList(secondaryKeywords);
|
||||||
|
if (!badges) return null;
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium text-gray-600 dark:text-gray-400">Secondary Keywords:</span>
|
||||||
|
{badges}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const badges = renderBadgeList(tags);
|
||||||
|
if (!badges) return null;
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium text-gray-600 dark:text-gray-400">Tags:</span>
|
||||||
|
{badges}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const badges = renderBadgeList(categories);
|
||||||
|
if (!badges) return null;
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium text-gray-600 dark:text-gray-400">Categories:</span>
|
||||||
|
{badges}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{metaDescription && (
|
||||||
|
<div className="rounded-lg bg-gray-50 dark:bg-gray-800/50 p-3 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<span className="font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 block mb-1">
|
||||||
|
Meta Description
|
||||||
|
</span>
|
||||||
|
{metaDescription}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
<div className="html-content-wrapper">
|
<div className="html-content-wrapper">
|
||||||
<HTMLContentRenderer
|
<HTMLContentRenderer
|
||||||
content={content}
|
content={content}
|
||||||
|
|||||||
@@ -43,7 +43,14 @@ export default function Content() {
|
|||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderBadgeList = (items?: string[], emptyLabel = '-') => {
|
const getList = (primary?: string[], fallback?: any): string[] => {
|
||||||
|
if (primary && primary.length > 0) return primary;
|
||||||
|
if (!fallback) return [];
|
||||||
|
if (Array.isArray(fallback)) return fallback;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBadgeList = (items: string[], emptyLabel = '-') => {
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return <span className="text-gray-400 dark:text-gray-500">{emptyLabel}</span>;
|
return <span className="text-gray-400 dark:text-gray-500">{emptyLabel}</span>;
|
||||||
}
|
}
|
||||||
@@ -112,11 +119,18 @@ export default function Content() {
|
|||||||
<tbody className="divide-y divide-gray-200 dark:divide-white/[0.05]">
|
<tbody className="divide-y divide-gray-200 dark:divide-white/[0.05]">
|
||||||
{content.map((item) => {
|
{content.map((item) => {
|
||||||
const isExpanded = expandedId === item.id;
|
const isExpanded = expandedId === item.id;
|
||||||
return (
|
const secondaryKeywords = getList(
|
||||||
|
item.secondary_keywords,
|
||||||
|
item.metadata?.secondary_keywords
|
||||||
|
);
|
||||||
|
const tags = getList(item.tags, item.metadata?.tags);
|
||||||
|
const categories = getList(item.categories, item.metadata?.categories);
|
||||||
|
|
||||||
|
return (
|
||||||
<tr key={item.id} className="bg-white dark:bg-gray-900">
|
<tr key={item.id} className="bg-white dark:bg-gray-900">
|
||||||
<td className="px-5 py-4 align-top">
|
<td className="px-5 py-4 align-top">
|
||||||
<div className="font-medium text-gray-900 dark:text-white">
|
<div className="font-medium text-gray-900 dark:text-white">
|
||||||
{item.meta_title || item.title || `Task #${item.task}`}
|
{item.meta_title || item.title || item.task_title || `Task #${item.task}`}
|
||||||
</div>
|
</div>
|
||||||
{item.meta_description && (
|
{item.meta_description && (
|
||||||
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||||
@@ -134,13 +148,13 @@ export default function Content() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 align-top">
|
<td className="px-5 py-4 align-top">
|
||||||
{renderBadgeList(item.secondary_keywords)}
|
{renderBadgeList(secondaryKeywords)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 align-top">
|
<td className="px-5 py-4 align-top">
|
||||||
{renderBadgeList(item.tags)}
|
{renderBadgeList(tags)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 align-top">
|
<td className="px-5 py-4 align-top">
|
||||||
{renderBadgeList(item.categories)}
|
{renderBadgeList(categories)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 align-top text-gray-700 dark:text-gray-300">
|
<td className="px-5 py-4 align-top text-gray-700 dark:text-gray-300">
|
||||||
{item.word_count?.toLocaleString?.() ?? '-'}
|
{item.word_count?.toLocaleString?.() ?? '-'}
|
||||||
|
|||||||
Reference in New Issue
Block a user