ui improvements

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-27 06:08:29 +00:00
parent 726d945bda
commit 302af6337e
14 changed files with 219 additions and 211 deletions

View File

@@ -0,0 +1,303 @@
/**
* Approved Page Configuration
* Centralized config for Approved page table, filters, and actions
*/
import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date';
import { CheckCircleIcon, ArrowRightIcon } from '../../icons';
import { STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
export interface ColumnConfig {
key: string;
label: string;
sortable?: boolean;
sortField?: string;
align?: 'left' | 'center' | 'right';
width?: string;
numeric?: boolean;
date?: boolean;
render?: (value: any, row: any) => React.ReactNode;
toggleable?: boolean;
toggleContentKey?: string;
toggleContentLabel?: string;
defaultVisible?: boolean;
}
export interface FilterConfig {
key: string;
label: string;
type: 'text' | 'select';
placeholder?: string;
options?: Array<{ value: string; label: string }>;
}
export interface HeaderMetricConfig {
label: string;
accentColor: 'blue' | 'green' | 'amber' | 'purple';
calculate: (data: { content: Content[]; totalCount: number }) => number;
}
export interface ApprovedPageConfig {
columns: ColumnConfig[];
filters: FilterConfig[];
headerMetrics: HeaderMetricConfig[];
}
export function createApprovedPageConfig(params: {
searchTerm: string;
setSearchTerm: (value: string) => void;
publishStatusFilter: string;
setPublishStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void;
activeSector: { id: number; name: string } | null;
onRowClick?: (row: Content) => void;
}): ApprovedPageConfig {
const showSectorColumn = !params.activeSector;
const columns: ColumnConfig[] = [
{
key: 'title',
label: 'Title',
sortable: true,
sortField: 'title',
render: (value: string, row: Content) => (
<div className="flex items-center gap-2">
{params.onRowClick ? (
<button
onClick={() => params.onRowClick!(row)}
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
>
{value || `Content #${row.id}`}
</button>
) : (
<span className="text-base font-light text-gray-900 dark:text-white">
{value || `Content #${row.id}`}
</span>
)}
{row.external_url && (
<a
href={row.external_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 transition-colors"
title="View on WordPress"
>
<ArrowRightIcon className="w-4 h-4" />
</a>
)}
</div>
),
},
{
key: 'wordpress_status',
label: 'Site Status',
sortable: false,
width: '120px',
render: (_value: any, row: Content) => {
// Check if content has been published to WordPress
if (!row.external_id) {
return (
<Badge color="amber" size="xs" variant="soft">
<span className="text-[11px] font-normal">Not Published</span>
</Badge>
);
}
// WordPress status badge - use external_status if available, otherwise show 'Published'
const wpStatus = (row as any).wordpress_status || 'publish';
const statusConfig: Record<string, { color: 'success' | 'amber' | 'blue' | 'gray' | 'red'; label: string }> = {
publish: { color: 'success', label: 'Published' },
draft: { color: 'gray', label: 'Draft' },
pending: { color: 'amber', label: 'Pending' },
future: { color: 'blue', label: 'Scheduled' },
private: { color: 'amber', label: 'Private' },
trash: { color: 'red', label: 'Trashed' },
};
const config = statusConfig[wpStatus] || { color: 'success' as const, label: 'Published' };
return (
<Badge color={config.color} size="xs" variant="soft">
<span className="text-[11px] font-normal">{config.label}</span>
</Badge>
);
},
},
{
key: 'content_type',
label: 'Type',
sortable: false, // Backend doesn't support sorting by content_type
sortField: 'content_type',
width: '110px',
render: (value: string) => {
const label = TYPE_LABELS[value] || value || '-';
const properCase = label.charAt(0).toUpperCase() + label.slice(1);
return (
<Badge color="blue" size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span>
</Badge>
);
},
},
{
key: 'content_structure',
label: 'Structure',
sortable: false, // Backend doesn't support sorting by content_structure
sortField: 'content_structure',
width: '130px',
render: (value: string) => {
const label = STRUCTURE_LABELS[value] || value || '-';
const properCase = label.split(/[_\s]+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
return (
<Badge color="purple" size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span>
</Badge>
);
},
},
{
key: 'cluster_name',
label: 'Cluster',
sortable: false,
width: '130px',
render: (_value: any, row: Content) => {
const clusterName = row.cluster_name;
if (!clusterName) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
}
return (
<Badge color="indigo" size="xs" variant="soft">
<span className="text-[11px] font-normal">{clusterName}</span>
</Badge>
);
},
},
{
key: 'tags',
label: 'Tags',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const tags = row.tags || [];
if (!tags || tags.length === 0) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{tags.slice(0, 2).map((tag, index) => (
<Badge key={`${tag}-${index}`} color="pink" size="xs" variant="soft">
<span className="text-[11px] font-normal">{tag}</span>
</Badge>
))}
{tags.length > 2 && (
<span className="text-[11px] text-gray-500">+{tags.length - 2}</span>
)}
</div>
);
},
},
{
key: 'categories',
label: 'Categories',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const categories = row.categories || [];
if (!categories || categories.length === 0) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{categories.slice(0, 2).map((category, index) => (
<Badge key={`${category}-${index}`} color="blue" size="xs" variant="soft">
<span className="text-[11px] font-normal">{category}</span>
</Badge>
))}
{categories.length > 2 && (
<span className="text-[11px] text-gray-500">+{categories.length - 2}</span>
)}
</div>
);
},
},
{
key: 'word_count',
label: 'Words',
sortable: false, // Backend doesn't support sorting by word_count
sortField: 'word_count',
numeric: true,
width: '100px',
align: 'right' as const,
render: (value: number) => (
<span className="text-gray-900 dark:text-white">
{value ? value.toLocaleString() : '-'}
</span>
),
},
{
key: 'created_at',
label: 'Created',
sortable: true,
sortField: 'created_at',
date: true,
width: '140px',
render: (value: string) => (
<span className="text-gray-600 dark:text-gray-400">
{formatRelativeDate(value)}
</span>
),
},
];
const filters: FilterConfig[] = [
{
key: 'search',
label: 'Search',
type: 'text',
placeholder: 'Search approved content...',
},
{
key: 'publishStatus',
label: 'Site Status',
type: 'select',
options: [
{ value: '', label: 'All' },
{ value: 'published', label: 'Published to Site' },
{ value: 'not_published', label: 'Not Published' },
],
},
];
const headerMetrics: HeaderMetricConfig[] = [
{
label: 'Approved',
accentColor: 'green',
calculate: (data: { totalCount: number }) => data.totalCount,
tooltip: 'Total approved content ready for publishing.',
},
{
label: 'On Site',
accentColor: 'blue',
calculate: (data: { content: Content[] }) =>
data.content.filter(c => c.external_id).length,
tooltip: 'Content published to your website.',
},
{
label: 'Pending',
accentColor: 'amber',
calculate: (data: { content: Content[] }) =>
data.content.filter(c => !c.external_id).length,
tooltip: 'Approved content not yet published to site.',
},
];
return {
columns,
filters,
headerMetrics,
};
}