mpre ui fixes
This commit is contained in:
183
frontend/src/components/common/StatusMetricsCard.tsx
Normal file
183
frontend/src/components/common/StatusMetricsCard.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* StatusMetricsCard Component
|
||||
* Displays status metrics in a card format with colored left border
|
||||
* Used in table action rows to show page-specific status metrics
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export interface StatusMetricItem {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface StatusMetricsCardProps {
|
||||
/** Title for the card */
|
||||
title: string;
|
||||
/** Subtitle text shown below the main count */
|
||||
subtitle?: string;
|
||||
/** Icon to display */
|
||||
icon?: React.ReactNode;
|
||||
/** Color variant - matches page badge colors */
|
||||
color: 'blue' | 'orange' | 'pink' | 'emerald' | 'green' | 'purple' | 'amber' | 'red' | 'indigo' | 'cyan' | 'teal';
|
||||
/** Main count value */
|
||||
count: number;
|
||||
/** Array of metric items for 2-column layout */
|
||||
metrics?: StatusMetricItem[];
|
||||
/** Review count to display */
|
||||
reviewCount?: number;
|
||||
/** Link to review page */
|
||||
reviewLink?: string;
|
||||
/** Custom action button */
|
||||
actionButton?: {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
}
|
||||
|
||||
const colorClasses: Record<string, { border: string; bg: string; text: string; iconBg: string }> = {
|
||||
blue: {
|
||||
border: 'border-l-blue-500',
|
||||
bg: 'bg-blue-50 dark:bg-blue-500/10',
|
||||
text: 'text-blue-700 dark:text-blue-300',
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-500/20'
|
||||
},
|
||||
orange: {
|
||||
border: 'border-l-orange-500',
|
||||
bg: 'bg-orange-50 dark:bg-orange-500/10',
|
||||
text: 'text-orange-700 dark:text-orange-300',
|
||||
iconBg: 'bg-orange-100 dark:bg-orange-500/20'
|
||||
},
|
||||
pink: {
|
||||
border: 'border-l-pink-500',
|
||||
bg: 'bg-pink-50 dark:bg-pink-500/10',
|
||||
text: 'text-pink-700 dark:text-pink-300',
|
||||
iconBg: 'bg-pink-100 dark:bg-pink-500/20'
|
||||
},
|
||||
emerald: {
|
||||
border: 'border-l-emerald-500',
|
||||
bg: 'bg-emerald-50 dark:bg-emerald-500/10',
|
||||
text: 'text-emerald-700 dark:text-emerald-300',
|
||||
iconBg: 'bg-emerald-100 dark:bg-emerald-500/20'
|
||||
},
|
||||
green: {
|
||||
border: 'border-l-green-500',
|
||||
bg: 'bg-green-50 dark:bg-green-500/10',
|
||||
text: 'text-green-700 dark:text-green-300',
|
||||
iconBg: 'bg-green-100 dark:bg-green-500/20'
|
||||
},
|
||||
purple: {
|
||||
border: 'border-l-purple-500',
|
||||
bg: 'bg-purple-50 dark:bg-purple-500/10',
|
||||
text: 'text-purple-700 dark:text-purple-300',
|
||||
iconBg: 'bg-purple-100 dark:bg-purple-500/20'
|
||||
},
|
||||
amber: {
|
||||
border: 'border-l-amber-500',
|
||||
bg: 'bg-amber-50 dark:bg-amber-500/10',
|
||||
text: 'text-amber-700 dark:text-amber-300',
|
||||
iconBg: 'bg-amber-100 dark:bg-amber-500/20'
|
||||
},
|
||||
red: {
|
||||
border: 'border-l-red-500',
|
||||
bg: 'bg-red-50 dark:bg-red-500/10',
|
||||
text: 'text-red-700 dark:text-red-300',
|
||||
iconBg: 'bg-red-100 dark:bg-red-500/20'
|
||||
},
|
||||
indigo: {
|
||||
border: 'border-l-indigo-500',
|
||||
bg: 'bg-indigo-50 dark:bg-indigo-500/10',
|
||||
text: 'text-indigo-700 dark:text-indigo-300',
|
||||
iconBg: 'bg-indigo-100 dark:bg-indigo-500/20'
|
||||
},
|
||||
cyan: {
|
||||
border: 'border-l-cyan-500',
|
||||
bg: 'bg-cyan-50 dark:bg-cyan-500/10',
|
||||
text: 'text-cyan-700 dark:text-cyan-300',
|
||||
iconBg: 'bg-cyan-100 dark:bg-cyan-500/20'
|
||||
},
|
||||
teal: {
|
||||
border: 'border-l-teal-500',
|
||||
bg: 'bg-teal-50 dark:bg-teal-500/10',
|
||||
text: 'text-teal-700 dark:text-teal-300',
|
||||
iconBg: 'bg-teal-100 dark:bg-teal-500/20'
|
||||
},
|
||||
};
|
||||
|
||||
export default function StatusMetricsCard({
|
||||
title,
|
||||
subtitle,
|
||||
icon,
|
||||
color,
|
||||
count,
|
||||
metrics,
|
||||
reviewCount,
|
||||
reviewLink = '/writer/review',
|
||||
actionButton,
|
||||
}: StatusMetricsCardProps) {
|
||||
const colors = colorClasses[color] || colorClasses.blue;
|
||||
|
||||
return (
|
||||
<div className={`${colors.bg} border-l-4 ${colors.border} rounded-lg px-4 py-3 min-w-[280px]`}>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
{icon && (
|
||||
<div className={`${colors.iconBg} rounded-lg p-2 flex-shrink-0`}>
|
||||
<div className={`${colors.text} w-5 h-5`}>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Title and Count Row */}
|
||||
<div className="flex items-center justify-between gap-4 mb-1">
|
||||
<h4 className={`text-sm font-semibold ${colors.text}`}>{title}</h4>
|
||||
<span className="text-2xl font-bold text-gray-900 dark:text-white">{count}</span>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
{subtitle && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">{subtitle}</p>
|
||||
)}
|
||||
|
||||
{/* 2-column metrics */}
|
||||
{metrics && metrics.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 mt-2 text-sm">
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={index} className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">{metric.label}</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">{metric.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Review link/button */}
|
||||
{(reviewCount !== undefined || actionButton) && (
|
||||
<div className="mt-3 pt-2 border-t border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between gap-2">
|
||||
{reviewCount !== undefined && (
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-semibold text-gray-900 dark:text-white">{reviewCount}</span> awaiting review
|
||||
</span>
|
||||
)}
|
||||
{actionButton && (
|
||||
<Link
|
||||
to={actionButton.href}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 rounded-md transition-colors"
|
||||
>
|
||||
{actionButton.label}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export function createApprovedPageConfig(params: {
|
||||
const columns: ColumnConfig[] = [
|
||||
{
|
||||
key: 'title',
|
||||
label: 'Title',
|
||||
label: 'Content Idea Title',
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
render: (value: string, row: Content) => (
|
||||
@@ -92,7 +92,7 @@ export function createApprovedPageConfig(params: {
|
||||
},
|
||||
{
|
||||
key: 'wordpress_status',
|
||||
label: 'Site Status',
|
||||
label: 'Site Content Status',
|
||||
sortable: false,
|
||||
width: '120px',
|
||||
render: (_value: any, row: Content) => {
|
||||
|
||||
@@ -127,7 +127,7 @@ export const createClustersPageConfig = (
|
||||
}] : []),
|
||||
{
|
||||
key: 'keywords_count',
|
||||
label: 'Keywords',
|
||||
label: 'KW Count',
|
||||
sortable: true,
|
||||
sortField: 'keywords_count',
|
||||
width: '120px',
|
||||
@@ -141,17 +141,7 @@ export const createClustersPageConfig = (
|
||||
sortField: 'ideas_count',
|
||||
width: '120px',
|
||||
align: 'center' as const,
|
||||
render: (value: number) => (
|
||||
<Badge
|
||||
color={value > 0 ? 'success' : 'light'}
|
||||
size="xs"
|
||||
variant="soft"
|
||||
>
|
||||
<span className="text-[11px] font-normal">
|
||||
{value > 0 ? `${value.toLocaleString()} ideas` : 'No ideas'}
|
||||
</span>
|
||||
</Badge>
|
||||
),
|
||||
render: (value: number) => value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
key: 'volume',
|
||||
@@ -244,7 +234,7 @@ export const createClustersPageConfig = (
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
label: 'Modified',
|
||||
sortable: false, // Backend doesn't support sorting by updated_at
|
||||
sortField: 'updated_at',
|
||||
defaultVisible: false,
|
||||
@@ -253,7 +243,7 @@ export const createClustersPageConfig = (
|
||||
// Generate Ideas action column - only shows button for status = 'new'
|
||||
{
|
||||
key: 'generate_action',
|
||||
label: 'Actions',
|
||||
label: 'Generate Ideas',
|
||||
sortable: false,
|
||||
width: '120px',
|
||||
render: (_value: any, row: Cluster) => {
|
||||
|
||||
@@ -97,6 +97,7 @@ export const createContentPageConfig = (
|
||||
columns: [
|
||||
{
|
||||
...titleColumn,
|
||||
label: 'Content Idea Title',
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
render: (value: string, row: Content) => (
|
||||
@@ -385,7 +386,7 @@ export const createContentPageConfig = (
|
||||
// Optional columns - hidden by default
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
label: 'Modified',
|
||||
sortable: true,
|
||||
sortField: 'updated_at',
|
||||
defaultVisible: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ export const createIdeasPageConfig = (
|
||||
{
|
||||
...titleColumn,
|
||||
key: 'idea_title',
|
||||
label: 'Title',
|
||||
label: 'Content Idea Title',
|
||||
sortable: true,
|
||||
sortField: 'idea_title',
|
||||
toggleable: true, // Enable toggle for this column
|
||||
@@ -150,7 +150,7 @@ export const createIdeasPageConfig = (
|
||||
},
|
||||
{
|
||||
key: 'target_keywords',
|
||||
label: 'Target Keywords',
|
||||
label: 'Keywords',
|
||||
sortable: false,
|
||||
width: '250px',
|
||||
render: (value: string) => (
|
||||
@@ -215,7 +215,7 @@ export const createIdeasPageConfig = (
|
||||
// Optional columns - hidden by default
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
label: 'Modified',
|
||||
sortable: false, // Backend doesn't support sorting by updated_at
|
||||
sortField: 'updated_at',
|
||||
defaultVisible: false,
|
||||
|
||||
@@ -245,56 +245,11 @@ export const createKeywordsPageConfig = (
|
||||
},
|
||||
{
|
||||
...createdColumn,
|
||||
label: 'Added',
|
||||
sortable: true,
|
||||
sortField: 'created_at',
|
||||
render: (value: string) => formatRelativeDate(value),
|
||||
},
|
||||
// Optional columns - hidden by default
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
sortable: true,
|
||||
sortField: 'updated_at',
|
||||
defaultVisible: false,
|
||||
render: (value: string) => formatRelativeDate(value),
|
||||
},
|
||||
{
|
||||
key: 'volume_override',
|
||||
label: 'Volume Override',
|
||||
sortable: true,
|
||||
sortField: 'volume_override',
|
||||
defaultVisible: false,
|
||||
render: (value: number | null) => value ? value.toLocaleString() : '-',
|
||||
},
|
||||
{
|
||||
key: 'difficulty_override',
|
||||
label: 'Difficulty Override',
|
||||
sortable: true,
|
||||
sortField: 'difficulty_override',
|
||||
defaultVisible: false,
|
||||
align: 'center' as const,
|
||||
render: (value: number | null) => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
const difficultyNum = getDifficultyNumber(value);
|
||||
return typeof difficultyNum === 'number' ? (
|
||||
<Badge
|
||||
color={
|
||||
difficultyNum === 1 || difficultyNum === 2
|
||||
? 'success'
|
||||
: difficultyNum === 3
|
||||
? 'warning'
|
||||
: 'error'
|
||||
}
|
||||
variant={difficultyNum === 5 ? 'solid' : 'light'}
|
||||
size="sm"
|
||||
>
|
||||
{difficultyNum}
|
||||
</Badge>
|
||||
) : (
|
||||
difficultyNum
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ export function createReviewPageConfig(params: {
|
||||
const columns: ColumnConfig[] = [
|
||||
{
|
||||
key: 'title',
|
||||
label: 'Title',
|
||||
label: 'Content Idea Title',
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
render: (value: string, row: Content) => (
|
||||
|
||||
@@ -101,6 +101,7 @@ export const createTasksPageConfig = (
|
||||
columns: [
|
||||
{
|
||||
...titleColumn,
|
||||
label: 'Content Idea Title',
|
||||
sortable: true,
|
||||
sortField: 'title',
|
||||
toggleable: true,
|
||||
@@ -295,7 +296,7 @@ export const createTasksPageConfig = (
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
label: 'Modified',
|
||||
sortable: true,
|
||||
sortField: 'updated_at',
|
||||
defaultVisible: false,
|
||||
|
||||
@@ -12,7 +12,7 @@ export const titleColumn = {
|
||||
|
||||
export const keywordColumn = {
|
||||
key: 'keyword',
|
||||
label: 'Search Term',
|
||||
label: 'Keyword',
|
||||
sortable: true,
|
||||
width: 'auto',
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const statusColumn = {
|
||||
|
||||
export const volumeColumn = {
|
||||
key: 'volume',
|
||||
label: 'Monthly Searches',
|
||||
label: 'Volume',
|
||||
sortable: true,
|
||||
numeric: true,
|
||||
width: '100px',
|
||||
@@ -35,7 +35,7 @@ export const volumeColumn = {
|
||||
|
||||
export const difficultyColumn = {
|
||||
key: 'difficulty',
|
||||
label: 'Competition Level',
|
||||
label: 'Difficulty',
|
||||
sortable: true,
|
||||
badge: true,
|
||||
width: '120px',
|
||||
@@ -43,7 +43,7 @@ export const difficultyColumn = {
|
||||
|
||||
export const countryColumn = {
|
||||
key: 'country',
|
||||
label: 'Target Location',
|
||||
label: 'Country',
|
||||
sortable: true,
|
||||
badge: true,
|
||||
width: '120px',
|
||||
@@ -51,7 +51,7 @@ export const countryColumn = {
|
||||
|
||||
export const clusterColumn = {
|
||||
key: 'cluster',
|
||||
label: 'Topic Group',
|
||||
label: 'Cluster',
|
||||
sortable: true,
|
||||
width: '200px',
|
||||
};
|
||||
@@ -66,7 +66,7 @@ export const createdColumn = {
|
||||
|
||||
export const updatedColumn = {
|
||||
key: 'updated_at',
|
||||
label: 'Updated',
|
||||
label: 'Modified',
|
||||
sortable: true,
|
||||
date: true,
|
||||
width: '150px',
|
||||
@@ -82,7 +82,7 @@ export const actionsColumn = {
|
||||
|
||||
export const wordCountColumn = {
|
||||
key: 'word_count',
|
||||
label: 'Word Count',
|
||||
label: 'Words',
|
||||
sortable: true,
|
||||
numeric: true,
|
||||
width: '120px',
|
||||
@@ -90,7 +90,7 @@ export const wordCountColumn = {
|
||||
|
||||
export const sectorColumn = {
|
||||
key: 'sector_name',
|
||||
label: 'Topic Area',
|
||||
label: 'Sector',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { usePageContext } from "../context/PageContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { ThemeToggleButton } from "../components/common/ThemeToggleButton";
|
||||
import NotificationDropdown from "../components/header/NotificationDropdown";
|
||||
import UserDropdown from "../components/header/UserDropdown";
|
||||
@@ -29,6 +30,7 @@ const AppHeader: React.FC = () => {
|
||||
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const { pageInfo } = usePageContext();
|
||||
const { isExpanded, toggleSidebar } = useSidebar();
|
||||
|
||||
const toggleApplicationMenu = () => {
|
||||
setApplicationMenuOpen(!isApplicationMenuOpen);
|
||||
@@ -71,6 +73,22 @@ const AppHeader: React.FC = () => {
|
||||
{/* Page Title with Badge - Desktop */}
|
||||
{pageInfo && (
|
||||
<div className="hidden lg:flex items-center gap-3">
|
||||
{/* Sidebar Toggle Button - Always visible on desktop */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex items-center justify-center w-6 h-6 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-full shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{pageInfo.badge && (
|
||||
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${badgeColors[pageInfo.badge.color]?.bg || 'bg-gray-600'} flex-shrink-0`}>
|
||||
{pageInfo.badge.icon && typeof pageInfo.badge.icon === 'object' && 'type' in pageInfo.badge.icon
|
||||
|
||||
@@ -131,11 +131,11 @@ const AppSidebar: React.FC = () => {
|
||||
icon: <TaskIcon />,
|
||||
name: "Writer",
|
||||
subItems: [
|
||||
{ name: "Queue", path: "/writer/tasks" },
|
||||
{ name: "Drafts", path: "/writer/content" },
|
||||
{ name: "Images", path: "/writer/images" },
|
||||
{ name: "Review", path: "/writer/review" },
|
||||
{ name: "Approved", path: "/writer/approved" },
|
||||
{ name: "Content Queue", path: "/writer/tasks" },
|
||||
{ name: "Content Drafts", path: "/writer/content" },
|
||||
{ name: "Content Images", path: "/writer/images" },
|
||||
{ name: "Content Review", path: "/writer/review" },
|
||||
{ name: "Content Approved", path: "/writer/approved" },
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -452,25 +452,6 @@ const AppSidebar: React.FC = () => {
|
||||
onMouseEnter={() => !isExpanded && setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Collapse/Expand Toggle Button - Fixed 5px from sidebar edge, does not move with hover */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className={`hidden lg:flex absolute top-20 w-6 h-6 items-center justify-center bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-full shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors z-[60] ${
|
||||
isExpanded ? 'left-[295px]' : 'left-[95px]'
|
||||
}`}
|
||||
style={{ position: 'fixed' }}
|
||||
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="py-4 flex justify-center items-center">
|
||||
<Link to="/" className="flex justify-center items-center">
|
||||
{isExpanded || isHovered || isMobileOpen ? (
|
||||
|
||||
@@ -305,7 +305,7 @@ export default function Approved() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Approved"
|
||||
title="Content Approved"
|
||||
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
|
||||
parent="Writer"
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useProgressModal } from '../../hooks/useProgressModal';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { PencilSquareIcon } from '@heroicons/react/24/outline';
|
||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
||||
|
||||
export default function Content() {
|
||||
const toast = useToast();
|
||||
@@ -55,6 +56,22 @@ export default function Content() {
|
||||
const progressModal = useProgressModal();
|
||||
const hasReloadedRef = useRef(false);
|
||||
|
||||
// Review count state
|
||||
const [reviewCount, setReviewCount] = useState(0);
|
||||
|
||||
// Load review count
|
||||
useEffect(() => {
|
||||
const loadReviewCount = async () => {
|
||||
try {
|
||||
const data = await fetchContent({ status: 'review', page_size: 1 });
|
||||
setReviewCount(data.count || 0);
|
||||
} catch (error) {
|
||||
console.error('Error fetching review count:', error);
|
||||
}
|
||||
};
|
||||
loadReviewCount();
|
||||
}, []);
|
||||
|
||||
// Load content - wrapped in useCallback
|
||||
const loadContent = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -227,7 +244,7 @@ export default function Content() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Drafts"
|
||||
title="Content Drafts"
|
||||
badge={{ icon: <PencilSquareIcon />, color: 'orange' }}
|
||||
parent="Writer"
|
||||
/>
|
||||
@@ -274,26 +291,22 @@ export default function Content() {
|
||||
onBulkDelete={handleBulkDelete}
|
||||
getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`}
|
||||
statusExplainer={
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<div className="font-medium text-gray-700 dark:text-gray-300">
|
||||
Currently Generated (Draft): {content.filter(c => c.status === 'draft').length}
|
||||
</div>
|
||||
<div>
|
||||
Image Prompts: {content.filter(c => c.has_image_prompts).length}/{content.length}
|
||||
</div>
|
||||
<div>
|
||||
Images Generated: {content.filter(c => c.has_generated_images).length}/{content.length}
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-200 dark:border-gray-700 mt-2">
|
||||
<Link
|
||||
to="/writer/review"
|
||||
className="text-brand-500 hover:text-brand-600 flex items-center gap-1"
|
||||
>
|
||||
<span>Review ({content.filter(c => c.status === 'review').length})</span>
|
||||
<ArrowRightIcon className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<StatusMetricsCard
|
||||
title="Content Drafts"
|
||||
color="orange"
|
||||
icon={<PencilSquareIcon className="w-5 h-5" />}
|
||||
count={totalCount}
|
||||
subtitle="draft content items"
|
||||
metrics={[
|
||||
{ label: 'Image Prompts', value: `${content.filter(c => c.has_image_prompts).length}/${content.length}` },
|
||||
{ label: 'Images Generated', value: `${content.filter(c => c.has_generated_images).length}/${content.length}` },
|
||||
]}
|
||||
reviewCount={reviewCount}
|
||||
actionButton={{
|
||||
label: 'Review',
|
||||
href: '/writer/review',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
fetchAPI,
|
||||
deleteContent,
|
||||
bulkDeleteContent,
|
||||
fetchContent,
|
||||
} from '../../services/api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, DownloadIcon, ArrowRightIcon } from '../../icons';
|
||||
@@ -26,6 +27,7 @@ import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQu
|
||||
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import { Modal } from '../../components/ui/modal';
|
||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
||||
|
||||
export default function Images() {
|
||||
const toast = useToast();
|
||||
@@ -68,6 +70,22 @@ export default function Images() {
|
||||
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
||||
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
|
||||
|
||||
// Review count state
|
||||
const [reviewCount, setReviewCount] = useState(0);
|
||||
|
||||
// Load review count
|
||||
useEffect(() => {
|
||||
const loadReviewCount = async () => {
|
||||
try {
|
||||
const data = await fetchContent({ status: 'review', page_size: 1 });
|
||||
setReviewCount(data.count || 0);
|
||||
} catch (error) {
|
||||
console.error('Error fetching review count:', error);
|
||||
}
|
||||
};
|
||||
loadReviewCount();
|
||||
}, []);
|
||||
|
||||
// Load images - wrapped in useCallback
|
||||
const loadImages = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -451,7 +469,7 @@ export default function Images() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Images"
|
||||
title="Content Images"
|
||||
badge={{ icon: <PhotoIcon />, color: 'pink' }}
|
||||
parent="Writer"
|
||||
/>
|
||||
@@ -516,26 +534,22 @@ export default function Images() {
|
||||
}}
|
||||
onRowAction={handleRowAction}
|
||||
statusExplainer={
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<div className="font-medium text-gray-700 dark:text-gray-300">
|
||||
Content Images Status
|
||||
</div>
|
||||
<div>
|
||||
Need Images: {images.filter(i => i.overall_status === 'pending').length}
|
||||
</div>
|
||||
<div>
|
||||
Images Complete: {images.filter(i => i.overall_status === 'complete').length}
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-200 dark:border-gray-700 mt-2">
|
||||
<Link
|
||||
to="/writer/review"
|
||||
className="text-brand-500 hover:text-brand-600 flex items-center gap-1"
|
||||
>
|
||||
<span>Go to Review</span>
|
||||
<ArrowRightIcon className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<StatusMetricsCard
|
||||
title="Content Images"
|
||||
color="pink"
|
||||
icon={<PhotoIcon className="w-5 h-5" />}
|
||||
count={totalCount}
|
||||
subtitle="content items with images"
|
||||
metrics={[
|
||||
{ label: 'Need Images', value: images.filter(i => i.overall_status === 'pending').length },
|
||||
{ label: 'Images Complete', value: images.filter(i => i.overall_status === 'complete').length },
|
||||
]}
|
||||
reviewCount={reviewCount}
|
||||
actionButton={{
|
||||
label: 'Review',
|
||||
href: '/writer/review',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ImageQueueModal
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
||||
|
||||
export default function Review() {
|
||||
const toast = useToast();
|
||||
@@ -397,7 +398,7 @@ export default function Review() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Review"
|
||||
title="Content Review"
|
||||
badge={{ icon: <ClipboardDocumentCheckIcon />, color: 'emerald' }}
|
||||
parent="Writer"
|
||||
/>
|
||||
@@ -454,11 +455,17 @@ export default function Review() {
|
||||
}}
|
||||
onRowAction={handleRowAction}
|
||||
statusExplainer={
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<div className="font-medium text-gray-700 dark:text-gray-300">
|
||||
Approve {totalCount} content pages/articles awaiting approval
|
||||
</div>
|
||||
</div>
|
||||
<StatusMetricsCard
|
||||
title="In Review"
|
||||
color="emerald"
|
||||
icon={<ClipboardDocumentCheckIcon className="w-5 h-5" />}
|
||||
count={totalCount}
|
||||
subtitle="awaiting approval"
|
||||
actionButton={{
|
||||
label: 'Approved',
|
||||
href: '/writer/approved',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ModuleMetricsFooter
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchTasks,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
TaskCreateData,
|
||||
fetchClusters,
|
||||
Cluster,
|
||||
fetchContent,
|
||||
} from '../../services/api';
|
||||
import FormModal from '../../components/common/FormModal';
|
||||
import ProgressModal from '../../components/common/ProgressModal';
|
||||
@@ -31,6 +33,7 @@ import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
||||
import StatusMetricsCard from '../../components/common/StatusMetricsCard';
|
||||
|
||||
export default function Tasks() {
|
||||
const toast = useToast();
|
||||
@@ -79,11 +82,27 @@ export default function Tasks() {
|
||||
// Progress modal for AI functions
|
||||
const progressModal = useProgressModal();
|
||||
|
||||
// Review count state
|
||||
const [reviewCount, setReviewCount] = useState(0);
|
||||
|
||||
// AI Function Logs state
|
||||
|
||||
|
||||
const hasReloadedRef = useRef<boolean>(false);
|
||||
|
||||
// Load review count
|
||||
useEffect(() => {
|
||||
const loadReviewCount = async () => {
|
||||
try {
|
||||
const data = await fetchContent({ status: 'review', page_size: 1 });
|
||||
setReviewCount(data.count || 0);
|
||||
} catch (error) {
|
||||
console.error('Error fetching review count:', error);
|
||||
}
|
||||
};
|
||||
loadReviewCount();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
// Load clusters for filter dropdown
|
||||
@@ -367,7 +386,7 @@ export default function Tasks() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Queue"
|
||||
title="Content Queue"
|
||||
badge={{ icon: <DocumentTextIcon />, color: 'blue' }}
|
||||
parent="Writer"
|
||||
/>
|
||||
@@ -417,12 +436,6 @@ export default function Tasks() {
|
||||
setIsEditMode(true);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
onCreate={() => {
|
||||
resetForm();
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
createLabel="Add Task"
|
||||
onCreateIcon={<PlusIcon />}
|
||||
onDelete={async (id: number) => {
|
||||
await deleteTask(id);
|
||||
loadTasks();
|
||||
@@ -473,6 +486,24 @@ export default function Tasks() {
|
||||
setTypeFilter('');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
statusExplainer={
|
||||
<StatusMetricsCard
|
||||
title="In Queue"
|
||||
color="blue"
|
||||
icon={<DocumentTextIcon className="w-5 h-5" />}
|
||||
count={totalCount}
|
||||
subtitle="content items queued"
|
||||
metrics={[
|
||||
{ label: 'Queued', value: tasks.filter(t => t.status === 'queued').length },
|
||||
{ label: 'Processing', value: tasks.filter(t => t.status === 'in_progress').length },
|
||||
]}
|
||||
reviewCount={reviewCount}
|
||||
actionButton={{
|
||||
label: 'Review',
|
||||
href: '/writer/review',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
||||
|
||||
@@ -828,7 +828,7 @@ export default function TablePageTemplate({
|
||||
)}
|
||||
{visibleColumnsList.map((column, colIndex) => {
|
||||
const isLastColumn = colIndex === visibleColumnsList.length - 1;
|
||||
const displayName = formatColumnKey(column.key);
|
||||
const displayName = column.label || formatColumnKey(column.key);
|
||||
return (
|
||||
<TableCell
|
||||
key={column.key}
|
||||
@@ -854,7 +854,7 @@ export default function TablePageTemplate({
|
||||
<ColumnSelector
|
||||
columns={columns.map(col => ({
|
||||
key: col.key,
|
||||
label: formatColumnKey(col.key),
|
||||
label: col.label || formatColumnKey(col.key),
|
||||
defaultVisible: col.defaultVisible !== false,
|
||||
}))}
|
||||
visibleColumns={visibleColumns}
|
||||
|
||||
Reference in New Issue
Block a user