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

@@ -405,15 +405,13 @@ export default function Clusters() {
volumeMin: volumeMin,
volumeMax: volumeMax,
}}
nextAction={selectedIds.length > 0 ? {
primaryAction={{
label: 'Generate Ideas',
message: `${selectedIds.length} selected`,
onClick: () => handleBulkAction('generate_ideas', selectedIds),
} : clusters.length > 0 ? {
label: 'Generate Ideas',
href: '/planner/ideas',
message: `${clusters.length} clusters`,
} : undefined}
icon: <BoltIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('auto_generate_ideas', selectedIds),
variant: 'success',
}}
getRowClassName={(row) => (row.ideas_count || 0) > 0 ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {

View File

@@ -23,7 +23,7 @@ import FormModal from '../../components/common/FormModal';
import ProgressModal from '../../components/common/ProgressModal';
import { useProgressModal } from '../../hooks/useProgressModal';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { BoltIcon, PlusIcon, DownloadIcon, ListIcon, GroupIcon } from '../../icons';
import { BoltIcon, PlusIcon, DownloadIcon, ListIcon, GroupIcon, ArrowRightIcon } from '../../icons';
import { LightBulbIcon } from '@heroicons/react/24/outline';
import { createIdeasPageConfig } from '../../config/pages/ideas.config';
import { useSectorStore } from '../../store/sectorStore';
@@ -316,15 +316,13 @@ export default function Ideas() {
content_structure: structureFilter,
content_type: typeFilter,
}}
nextAction={selectedIds.length > 0 ? {
primaryAction={{
label: 'Queue to Writer',
message: `${selectedIds.length} selected`,
icon: <ArrowRightIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('queue_to_writer', selectedIds),
} : ideas.filter(i => i.status === 'approved').length > 0 ? {
label: 'Start Writing',
href: '/writer/queue',
message: `${ideas.filter(i => i.status === 'approved').length} approved`,
} : undefined}
variant: 'success',
}}
getRowClassName={(row) => row.status === 'queued' || row.status === 'completed' ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {

View File

@@ -597,19 +597,13 @@ export default function Keywords() {
volumeMin: volumeMin,
volumeMax: volumeMax,
}}
nextAction={selectedIds.length > 0 ? {
label: 'Auto-Cluster Selected',
message: `${selectedIds.length} selected`,
onClick: handleAutoCluster,
} : workflowStats.unclustered >= 5 ? {
label: 'Auto-Cluster All',
message: `${workflowStats.unclustered} unclustered`,
onClick: handleAutoCluster,
} : workflowStats.clustered > 0 ? {
label: 'Generate Ideas',
href: '/planner/ideas',
message: `${workflowStats.clustered} clustered`,
} : undefined}
primaryAction={{
label: 'Auto-Cluster',
icon: <BoltIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('auto_cluster', selectedIds),
variant: 'success',
}}
getRowClassName={(row) => row.cluster_id ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key, value) => {
// Normalize value to string, preserving empty strings
const stringValue = value === null || value === undefined ? '' : String(value);

View File

@@ -1,6 +1,6 @@
/**
* Published Page - Built with TablePageTemplate
* Shows published/review content with WordPress publishing capabilities
* Approved Page - Built with TablePageTemplate
* Shows approved content ready for publishing to WordPress/external sites
*/
import { useState, useEffect, useMemo, useCallback } from 'react';
@@ -17,15 +17,15 @@ import {
bulkDeleteContent,
} from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, CheckCircleIcon } from '../../icons';
import { FileIcon, CheckCircleIcon, BoltIcon } from '../../icons';
import { RocketLaunchIcon } from '@heroicons/react/24/outline';
import { createPublishedPageConfig } from '../../config/pages/published.config';
import { createApprovedPageConfig } from '../../config/pages/approved.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader';
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
export default function Published() {
export default function Approved() {
const toast = useToast();
const navigate = useNavigate();
const { activeSector } = useSectorStore();
@@ -35,9 +35,9 @@ export default function Published() {
const [content, setContent] = useState<Content[]>([]);
const [loading, setLoading] = useState(true);
// Filter state - default to published/review status
// Filter state - default to approved status
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('published'); // Default to published
const [statusFilter, setStatusFilter] = useState('approved'); // Default to approved
const [publishStatusFilter, setPublishStatusFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
@@ -51,7 +51,7 @@ export default function Published() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// Load content - filtered for published/review
// Load content - filtered for approved status
const loadContent = useCallback(async () => {
setLoading(true);
setShowContent(false);
@@ -60,7 +60,7 @@ export default function Published() {
const filters: ContentFilters = {
...(searchTerm && { search: searchTerm }),
...(statusFilter && { status: statusFilter }),
status: 'approved', // Always filter for approved status
page: currentPage,
page_size: pageSize,
ordering,
@@ -280,11 +280,9 @@ export default function Published() {
// Create page config
const pageConfig = useMemo(() => {
return createPublishedPageConfig({
return createApprovedPageConfig({
searchTerm,
setSearchTerm,
statusFilter,
setStatusFilter,
publishStatusFilter,
setPublishStatusFilter,
setCurrentPage,
@@ -293,7 +291,7 @@ export default function Published() {
navigate(`/writer/content/${row.id}`);
},
});
}, [searchTerm, statusFilter, publishStatusFilter, activeSector, navigate]);
}, [searchTerm, publishStatusFilter, activeSector, navigate]);
// Calculate header metrics
const headerMetrics = useMemo(() => {
@@ -308,8 +306,8 @@ export default function Published() {
return (
<>
<PageHeader
title="Published"
badge={{ icon: <RocketLaunchIcon />, color: 'green' }}
title="Approved"
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
parent="Writer"
/>
<TablePageTemplate
@@ -320,24 +318,18 @@ export default function Published() {
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
publishStatus: publishStatusFilter,
}}
nextAction={selectedIds.length > 0 ? {
label: 'Sync to WordPress',
message: `${selectedIds.length} selected`,
onClick: () => handleBulkAction('publish_to_wordpress', selectedIds),
} : {
label: 'Create More Content',
href: '/planner/keywords',
message: `${content.length} published`,
primaryAction={{
label: 'Publish to Site',
icon: <BoltIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('bulk_publish_wordpress', selectedIds),
variant: 'success',
}}
getRowClassName={(row) => row.external_id ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key: string, value: any) => {
if (key === 'search') {
setSearchTerm(value);
} else if (key === 'status') {
setStatusFilter(value);
setCurrentPage(1);
} else if (key === 'publishStatus') {
setPublishStatusFilter(value);
setCurrentPage(1);
@@ -372,23 +364,23 @@ export default function Published() {
<ModuleMetricsFooter
metrics={[
{
title: 'Published Content',
value: content.filter(c => c.status === 'published').length.toLocaleString(),
subtitle: `${content.filter(c => c.external_id).length} on WordPress`,
title: 'Approved Content',
value: content.length.toLocaleString(),
subtitle: 'ready for publishing',
icon: <CheckCircleIcon className="w-5 h-5" />,
accentColor: 'green',
href: '/writer/published',
},
{
title: 'Draft Content',
value: content.filter(c => c.status === 'draft').length.toLocaleString(),
subtitle: 'Not yet published',
icon: <FileIcon className="w-5 h-5" />,
title: 'Published to Site',
value: content.filter(c => c.external_id).length.toLocaleString(),
subtitle: 'on WordPress',
icon: <RocketLaunchIcon className="w-5 h-5" />,
accentColor: 'blue',
href: '/writer/approved',
},
]}
progress={{
label: 'WordPress Publishing Progress',
label: 'Site Publishing Progress',
value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0,
color: 'success',
}}

View File

@@ -242,15 +242,7 @@ export default function Content() {
status: statusFilter,
source: sourceFilter,
}}
nextAction={selectedIds.length > 0 ? {
label: 'Generate Images',
message: `${selectedIds.length} selected`,
onClick: () => handleRowAction('generate_images', { id: selectedIds[0] }),
} : content.filter(c => c.status === 'draft').length > 0 ? {
label: 'Generate Images',
href: '/writer/images',
message: `${content.filter(c => c.status === 'draft').length} drafts`,
} : undefined}
getRowClassName={(row) => row.status === 'review' || row.status === 'published' ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key: string, value: any) => {
if (key === 'search') {
setSearchTerm(value);

View File

@@ -243,6 +243,52 @@ export default function Review() {
}
}, [loadContent, toast]);
// Approve content - single item (changes status from 'review' to 'approved')
const handleApproveSingle = useCallback(async (row: Content) => {
try {
await fetchAPI(`/v1/writer/content/${row.id}/`, {
method: 'PATCH',
body: JSON.stringify({ status: 'approved' })
});
toast.success(`Approved "${row.title}"`);
loadContent();
} catch (error: any) {
toast.error(`Failed to approve: ${error.message || 'Network error'}`);
}
}, [loadContent, toast]);
// Approve content - bulk (changes status from 'review' to 'approved')
const handleApproveBulk = useCallback(async (ids: string[]) => {
try {
let successCount = 0;
let failedCount = 0;
for (const id of ids) {
try {
await fetchAPI(`/v1/writer/content/${id}/`, {
method: 'PATCH',
body: JSON.stringify({ status: 'approved' })
});
successCount++;
} catch (error) {
failedCount++;
console.error(`Error approving content ${id}:`, error);
}
}
if (successCount > 0) {
toast.success(`Successfully approved ${successCount} item(s)`);
}
if (failedCount > 0) {
toast.warning(`${failedCount} item(s) failed to approve`);
}
loadContent();
} catch (error: any) {
toast.error(`Failed to approve: ${error.message || 'Network error'}`);
}
}, [loadContent, toast]);
// Publish to WordPress - bulk
const handlePublishBulk = useCallback(async (ids: string[]) => {
try {
@@ -291,21 +337,25 @@ export default function Review() {
// Bulk action handler
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'bulk_publish_wordpress') {
if (action === 'bulk_approve') {
await handleApproveBulk(ids);
} else if (action === 'bulk_publish_wordpress') {
await handlePublishBulk(ids);
} else {
toast.info(`Bulk action "${action}" for ${ids.length} items`);
}
}, [handlePublishBulk, toast]);
}, [handleApproveBulk, handlePublishBulk, toast]);
// Row action handler
const handleRowAction = useCallback(async (action: string, row: Content) => {
if (action === 'publish_wordpress') {
if (action === 'approve') {
await handleApproveSingle(row);
} else if (action === 'publish_wordpress') {
await handlePublishSingle(row);
} else if (action === 'view') {
navigate(`/writer/content/${row.id}`);
}
}, [handlePublishSingle, navigate]);
}, [handleApproveSingle, handlePublishSingle, navigate]);
// Delete handler (single)
const handleDelete = useCallback(async (id: string) => {
@@ -360,15 +410,13 @@ export default function Review() {
filterValues={{
search: searchTerm,
}}
nextAction={selectedIds.length > 0 ? {
label: 'Publish Selected',
message: `${selectedIds.length} selected`,
onClick: () => handleBulkAction('publish', selectedIds),
} : content.filter(c => c.status === 'review').length > 0 ? {
label: 'View Published',
href: '/writer/published',
message: `${content.filter(c => c.status === 'review').length} in review`,
} : undefined}
primaryAction={{
label: 'Approve Selected',
icon: <CheckCircleIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('bulk_approve', selectedIds),
variant: 'success',
}}
getRowClassName={(row) => row.status === 'approved' ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {

View File

@@ -385,15 +385,7 @@ export default function Tasks() {
content_type: typeFilter,
source: sourceFilter,
}}
nextAction={selectedIds.length > 0 ? {
label: 'Generate Content',
message: `${selectedIds.length} selected`,
onClick: () => handleBulkAction('generate_content', selectedIds),
} : tasks.filter(t => t.status === 'queued').length > 0 ? {
label: 'View Drafts',
href: '/writer/content',
message: `${tasks.filter(t => t.status === 'queued').length} queued`,
} : undefined}
getRowClassName={(row) => row.status === 'completed' ? 'bg-success-50 dark:bg-success-500/10' : ''}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {