Files
igny8/frontend/src/pages/Sites/PostEditor.tsx
2025-11-20 03:30:39 +05:00

851 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Post Editor (Advanced)
* Phase 7: Advanced Site Management
* Full-featured editing: SEO, metadata, tags, categories, HTML content
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { SaveIcon, XIcon, EyeIcon, FileTextIcon, SettingsIcon, TagIcon, CheckCircleIcon, XCircleIcon, AlertCircleIcon } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import TextArea from '../../components/form/input/TextArea';
import SelectDropdown from '../../components/form/SelectDropdown';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api';
interface Content {
id?: number;
title: string;
html_content?: string;
content?: string;
meta_title?: string;
meta_description?: string;
primary_keyword?: string;
secondary_keywords?: string[];
tags?: string[];
categories?: string[];
content_type: string;
status: string;
site: number;
sector: number;
word_count?: number;
metadata?: Record<string, any>;
// Stage 3: Metadata fields
entity_type?: string | null;
cluster_name?: string | null;
cluster_id?: number | null;
taxonomy_name?: string | null;
taxonomy_id?: number | null;
cluster_role?: string | null;
}
export default function PostEditor() {
const { id: siteId, postId } = useParams<{ id: string; postId?: string }>();
const navigate = useNavigate();
const toast = useToast();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeTab, setActiveTab] = useState<'content' | 'seo' | 'metadata' | 'validation'>('content');
const [validationResult, setValidationResult] = useState<ContentValidationResult | null>(null);
const [validating, setValidating] = useState(false);
const [content, setContent] = useState<Content>({
title: '',
html_content: '',
content: '',
meta_title: '',
meta_description: '',
primary_keyword: '',
secondary_keywords: [],
tags: [],
categories: [],
content_type: 'article',
status: 'draft',
site: Number(siteId),
sector: 0, // Will be set from site
});
const [tagInput, setTagInput] = useState('');
const [categoryInput, setCategoryInput] = useState('');
useEffect(() => {
if (siteId) {
loadSite();
if (postId && postId !== 'new') {
loadPost();
loadValidation();
} else {
setLoading(false);
}
}
}, [siteId, postId]);
const loadValidation = async () => {
if (!postId || postId === 'new') return;
try {
const result = await fetchContentValidation(Number(postId));
setValidationResult(result);
} catch (error: any) {
console.error('Failed to load validation:', error);
toast.error(`Failed to load validation: ${error.message || 'Unknown error'}`);
}
};
const handleValidate = async () => {
if (!content.id) {
toast.error('Please save the content first before validating');
return;
}
try {
setValidating(true);
const result = await validateContent(content.id);
await loadValidation();
if (result.is_valid) {
toast.success('Content validation passed!');
} else {
toast.warning(`Validation found ${result.errors.length} issue(s)`);
}
} catch (error: any) {
toast.error(`Validation failed: ${error.message}`);
} finally {
setValidating(false);
}
};
const loadSite = async () => {
try {
const site = await fetchAPI(`/v1/auth/sites/${siteId}/`);
if (site) {
setContent((prev) => ({
...prev,
sector: site.sector || 0,
}));
}
} catch (error: any) {
console.error('Failed to load site:', error);
}
};
const loadPost = async () => {
try {
setLoading(true);
const data = await fetchAPI(`/v1/writer/content/${postId}/`);
if (data) {
setContent({
id: data.id,
title: data.title || '',
html_content: data.html_content || '',
content: data.html_content || data.content || '',
meta_title: data.meta_title || '',
meta_description: data.meta_description || '',
primary_keyword: data.primary_keyword || '',
secondary_keywords: Array.isArray(data.secondary_keywords) ? data.secondary_keywords : [],
tags: Array.isArray(data.tags) ? data.tags : [],
categories: Array.isArray(data.categories) ? data.categories : [],
content_type: 'article', // Content model doesn't have content_type
status: data.status || 'draft',
site: data.site || Number(siteId),
sector: data.sector || 0,
word_count: data.word_count || 0,
metadata: data.metadata || {},
});
}
} catch (error: any) {
toast.error(`Failed to load post: ${error.message}`);
navigate(`/sites/${siteId}/content`);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!content.title.trim()) {
toast.error('Title is required');
return;
}
try {
setSaving(true);
const payload = {
...content,
html_content: content.html_content || content.content,
};
if (content.id) {
// Update existing
await fetchAPI(`/v1/writer/content/${content.id}/`, {
method: 'PUT',
body: JSON.stringify(payload),
});
toast.success('Post updated successfully');
} else {
// Create new - need to create a task first
const taskData = await fetchAPI('/v1/writer/tasks/', {
method: 'POST',
body: JSON.stringify({
title: content.title,
description: content.meta_description || '',
keywords: content.primary_keyword || '',
site_id: content.site,
sector_id: content.sector,
content_type: 'article',
content_structure: 'blog_post',
status: 'completed',
}),
});
if (taskData?.id) {
const result = await fetchAPI('/v1/writer/content/', {
method: 'POST',
body: JSON.stringify({
...payload,
task_id: taskData.id,
}),
});
toast.success('Post created successfully');
if (result?.id) {
navigate(`/sites/${siteId}/posts/${result.id}/edit`);
}
}
}
} catch (error: any) {
toast.error(`Failed to save post: ${error.message}`);
} finally {
setSaving(false);
}
};
const handleAddTag = () => {
if (tagInput.trim() && !content.tags?.includes(tagInput.trim())) {
setContent({
...content,
tags: [...(content.tags || []), tagInput.trim()],
});
setTagInput('');
}
};
const handleRemoveTag = (tag: string) => {
setContent({
...content,
tags: content.tags?.filter((t) => t !== tag) || [],
});
};
const handleAddCategory = () => {
if (categoryInput.trim() && !content.categories?.includes(categoryInput.trim())) {
setContent({
...content,
categories: [...(content.categories || []), categoryInput.trim()],
});
setCategoryInput('');
}
};
const handleRemoveCategory = (category: string) => {
setContent({
...content,
categories: content.categories?.filter((c) => c !== category) || [],
});
};
const CONTENT_TYPES = [
{ value: 'article', label: 'Article' },
{ value: 'blog_post', label: 'Blog Post' },
{ value: 'page', label: 'Page' },
{ value: 'product', label: 'Product' },
];
const STATUS_OPTIONS = [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' },
{ value: 'publish', label: 'Published' },
];
if (loading) {
return (
<div className="p-6">
<PageMeta title="Post Editor" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading post...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title={content.id ? 'Edit Post' : 'New Post'} />
<div className="flex gap-6">
{/* Main Content Area */}
<div className="flex-1">
<div className="mb-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{content.id ? 'Edit Post' : 'New Post'}
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
{content.id ? 'Edit your post content' : 'Create a new post'}
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => navigate(`/sites/${siteId}/content`)}
>
<XIcon className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={saving || (content.status === 'publish' && validationResult && !validationResult.is_valid)}
title={content.status === 'publish' && validationResult && !validationResult.is_valid ? 'Please fix validation errors before publishing' : undefined}
>
<SaveIcon className="w-4 h-4 mr-2" />
{saving ? 'Saving...' : content.status === 'publish' ? 'Publish' : 'Save Post'}
</Button>
</div>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
onClick={() => setActiveTab('content')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'content'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<FileTextIcon className="w-4 h-4 inline mr-2" />
Content
</button>
<button
type="button"
onClick={() => setActiveTab('seo')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'seo'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<EyeIcon className="w-4 h-4 inline mr-2" />
SEO
</button>
<button
type="button"
onClick={() => setActiveTab('metadata')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'metadata'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<TagIcon className="w-4 h-4 inline mr-2" />
Metadata
</button>
{content.id && (
<button
type="button"
onClick={() => {
setActiveTab('validation');
loadValidation();
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'validation'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<CheckCircleIcon className="w-4 h-4 inline mr-2" />
Validation
{validationResult && !validationResult.is_valid && (
<span className="ml-2 px-2 py-0.5 text-xs bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400 rounded-full">
{validationResult.validation_errors.length}
</span>
)}
</button>
)}
</div>
</div>
<div className="space-y-6">
{/* Content Tab */}
{activeTab === 'content' && (
<Card className="p-6">
<div className="space-y-4">
<div>
<Label>Title *</Label>
<input
type="text"
value={content.title}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="Enter post title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Content Type</Label>
<SelectDropdown
options={CONTENT_TYPES}
value={content.content_type}
onChange={(value) => setContent({ ...content, content_type: value })}
/>
</div>
<div>
<Label>Status</Label>
<SelectDropdown
options={STATUS_OPTIONS}
value={content.status}
onChange={(value) => setContent({ ...content, status: value })}
/>
</div>
</div>
<div>
<Label>Content (HTML)</Label>
<TextArea
value={content.html_content || content.content}
onChange={(value) => setContent({ ...content, html_content: value, content: value })}
rows={25}
placeholder="Write your post content here (HTML supported)..."
className="mt-1 font-mono text-sm"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
HTML content is supported. Use &lt;p&gt;, &lt;h1&gt;, &lt;h2&gt;, etc. for formatting.
</p>
</div>
{content.word_count !== undefined && content.word_count > 0 && (
<div className="text-sm text-gray-600 dark:text-gray-400">
Word count: {content.word_count.toLocaleString()}
</div>
)}
</div>
</Card>
)}
{/* SEO Tab */}
{activeTab === 'seo' && (
<Card className="p-6">
<div className="space-y-4">
<div>
<Label>Meta Title</Label>
<input
type="text"
value={content.meta_title || ''}
onChange={(e) => setContent({ ...content, meta_title: e.target.value })}
placeholder="SEO title (recommended: 50-60 characters)"
maxLength={60}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{content.meta_title?.length || 0}/60 characters
</p>
</div>
<div>
<Label>Meta Description</Label>
<TextArea
value={content.meta_description || ''}
onChange={(value) => setContent({ ...content, meta_description: value })}
rows={4}
placeholder="SEO description (recommended: 150-160 characters)"
maxLength={160}
className="mt-1"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{content.meta_description?.length || 0}/160 characters
</p>
</div>
<div>
<Label>Primary Keyword</Label>
<input
type="text"
value={content.primary_keyword || ''}
onChange={(e) => setContent({ ...content, primary_keyword: e.target.value })}
placeholder="Main keyword for this content"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Secondary Keywords (comma-separated)</Label>
<input
type="text"
value={content.secondary_keywords?.join(', ') || ''}
onChange={(e) => {
const keywords = e.target.value.split(',').map((k) => k.trim()).filter(Boolean);
setContent({ ...content, secondary_keywords: keywords });
}}
placeholder="keyword1, keyword2, keyword3"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
</Card>
)}
{/* Metadata Tab */}
{activeTab === 'metadata' && (
<Card className="p-6">
<div className="space-y-6">
<div>
<Label>Tags</Label>
<div className="mt-2 flex gap-2">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
}}
placeholder="Add a tag and press Enter"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<Button type="button" onClick={handleAddTag} variant="outline">
Add
</Button>
</div>
{content.tags && content.tags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{content.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full text-sm"
>
{tag}
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
×
</button>
</span>
))}
</div>
)}
</div>
<div>
<Label>Categories</Label>
<div className="mt-2 flex gap-2">
<input
type="text"
value={categoryInput}
onChange={(e) => setCategoryInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCategory();
}
}}
placeholder="Add a category and press Enter"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<Button type="button" onClick={handleAddCategory} variant="outline">
Add
</Button>
</div>
{content.categories && content.categories.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{content.categories.map((category) => (
<span
key={category}
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 dark:bg-blue-900 rounded-full text-sm"
>
{category}
<button
type="button"
onClick={() => handleRemoveCategory(category)}
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
>
×
</button>
</span>
))}
</div>
)}
</div>
</div>
</Card>
)}
{/* Validation Tab - Stage 3 */}
{activeTab === 'validation' && content.id && (
<Card className="p-6">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Content Validation
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Check if your content meets all requirements before publishing
</p>
</div>
<Button
variant="primary"
onClick={handleValidate}
disabled={validating}
aria-label="Run content validation"
>
{validating ? 'Validating...' : 'Run Validation'}
</Button>
</div>
{validationResult ? (
<div className="space-y-4">
{/* Validation Status */}
<div className={`p-4 rounded-lg ${
validationResult.is_valid
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
}`}>
<div className="flex items-center gap-2">
{validationResult.is_valid ? (
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
) : (
<XCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
)}
<span className={`font-medium ${
validationResult.is_valid
? 'text-green-800 dark:text-green-300'
: 'text-red-800 dark:text-red-300'
}`}>
{validationResult.is_valid
? 'Content is valid and ready to publish'
: `Content has ${validationResult.validation_errors.length} validation error(s)`}
</span>
</div>
</div>
{/* Metadata Summary */}
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4" role="region" aria-labelledby="metadata-summary-title">
<h4 id="metadata-summary-title" className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
Metadata Summary
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600 dark:text-gray-400">Entity Type:</span>
<span className="ml-2 font-medium text-gray-900 dark:text-white">
{validationResult.metadata.entity_type || 'Not set'}
</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Cluster Mapping:</span>
<span className={`ml-2 font-medium ${
validationResult.metadata.has_cluster_mapping
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{validationResult.metadata.has_cluster_mapping ? 'Yes' : 'No'}
</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Taxonomy Mapping:</span>
<span className={`ml-2 font-medium ${
validationResult.metadata.has_taxonomy_mapping
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{validationResult.metadata.has_taxonomy_mapping ? 'Yes' : 'No'}
</span>
</div>
</div>
</div>
{/* Validation Errors */}
{validationResult.validation_errors.length > 0 && (
<div role="alert" aria-live="polite">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
Validation Errors
</h4>
<div className="space-y-2">
{validationResult.validation_errors.map((error, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800"
>
<AlertCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-red-800 dark:text-red-300">
{error.field || error.code}
</div>
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
{error.message}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Publish Errors */}
{validationResult.publish_errors && validationResult.publish_errors.length > 0 && (
<div role="alert" aria-live="polite">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
Publish Blockers
</h4>
<div className="space-y-2">
{validationResult.publish_errors.map((error, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800"
>
<XCircleIcon className="w-5 h-5 text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-orange-800 dark:text-orange-300">
{error.field || error.code}
</div>
<div className="text-sm text-orange-600 dark:text-orange-400 mt-1">
{error.message}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-8">
<FileTextIcon className="w-12 h-12 text-gray-400 dark:text-gray-500 mx-auto mb-3 opacity-50" />
<p className="text-gray-500 dark:text-gray-400 mb-2">No validation results yet</p>
<p className="text-sm text-gray-400 dark:text-gray-500">Click "Run Validation" to check your content</p>
</div>
)}
</div>
</Card>
)}
</div>
</div>
{/* Stage 3: Sidebar with Metadata Summary */}
{content.id && (
<div className="w-full lg:w-80 flex-shrink-0 mt-6 lg:mt-0">
<Card className="p-4 sticky top-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Content Metadata
</h3>
<div className="space-y-4">
{/* Entity Type */}
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Entity Type
</div>
<div className="text-sm text-gray-900 dark:text-white">
{content.entity_type ? (
content.entity_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())
) : (
<span className="text-gray-400 dark:text-gray-500 italic">Not set</span>
)}
</div>
</div>
{/* Cluster */}
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Cluster
</div>
<div className="text-sm text-gray-900 dark:text-white">
{content.cluster_name ? (
<>
{content.cluster_name}
{content.cluster_role && (
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400">
({content.cluster_role})
</span>
)}
</>
) : (
<span className="text-gray-400 dark:text-gray-500 italic">Not assigned</span>
)}
</div>
</div>
{/* Taxonomy */}
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Taxonomy
</div>
<div className="text-sm text-gray-900 dark:text-white">
{content.taxonomy_name ? (
content.taxonomy_name
) : (
<span className="text-gray-400 dark:text-gray-500 italic">Not assigned</span>
)}
</div>
</div>
{/* Validation Status */}
{validationResult && (
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Validation Status
</div>
<div className={`text-sm font-medium ${
validationResult.is_valid
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{validationResult.is_valid ? '✓ Valid' : `${validationResult.validation_errors.length} error(s)`}
</div>
</div>
)}
{/* Quick Links */}
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
Quick Actions
</div>
<div className="space-y-1">
{content.cluster_id ? (
<button
onClick={() => navigate(`/planner/clusters/${content.cluster_id}`)}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline w-full text-left transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
aria-label="View cluster details"
>
View Cluster
</button>
) : (
<span className="text-xs text-gray-400 dark:text-gray-500 italic">No cluster assigned</span>
)}
{content.taxonomy_id ? (
<button
onClick={() => navigate(`/sites/builder?taxonomy=${content.taxonomy_id}`)}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline w-full text-left transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
aria-label="View taxonomy details"
>
View Taxonomy
</button>
) : (
<span className="text-xs text-gray-400 dark:text-gray-500 italic">No taxonomy assigned</span>
)}
</div>
</div>
</div>
</Card>
</div>
)}
</div>
</div>
);
}