more fixes ui

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-27 07:09:33 +00:00
parent d5bda678fd
commit 4482d2f4c4
13 changed files with 117 additions and 29 deletions

View File

@@ -19,6 +19,7 @@ import { Cluster } from '../../services/api';
import Input from '../../components/form/input/InputField'; import Input from '../../components/form/input/InputField';
import Label from '../../components/form/Label'; import Label from '../../components/form/Label';
import Button from '../../components/ui/button/Button'; import Button from '../../components/ui/button/Button';
import { BoltIcon } from '../../icons';
export interface ColumnConfig { export interface ColumnConfig {
key: string; key: string;
@@ -93,6 +94,7 @@ export const createClustersPageConfig = (
volumeDropdownRef: React.RefObject<HTMLDivElement | null>; volumeDropdownRef: React.RefObject<HTMLDivElement | null>;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
loadClusters: () => Promise<void>; loadClusters: () => Promise<void>;
onGenerateIdeas?: (clusterId: number) => void; // Handler for generate ideas button
} }
): ClustersPageConfig => { ): ClustersPageConfig => {
const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors
@@ -129,6 +131,7 @@ export const createClustersPageConfig = (
sortable: true, sortable: true,
sortField: 'keywords_count', sortField: 'keywords_count',
width: '120px', width: '120px',
align: 'center' as const,
render: (value: number) => value.toLocaleString(), render: (value: number) => value.toLocaleString(),
}, },
{ {
@@ -137,6 +140,7 @@ export const createClustersPageConfig = (
sortable: false, // Backend doesn't support sorting by ideas_count sortable: false, // Backend doesn't support sorting by ideas_count
sortField: 'ideas_count', sortField: 'ideas_count',
width: '120px', width: '120px',
align: 'center' as const,
render: (value: number) => ( render: (value: number) => (
<Badge <Badge
color={value > 0 ? 'success' : 'light'} color={value > 0 ? 'success' : 'light'}
@@ -155,6 +159,7 @@ export const createClustersPageConfig = (
sortable: true, sortable: true,
sortField: 'volume', sortField: 'volume',
width: '120px', width: '120px',
align: 'center' as const,
render: (value: number) => value.toLocaleString(), render: (value: number) => value.toLocaleString(),
}, },
{ {
@@ -193,6 +198,7 @@ export const createClustersPageConfig = (
sortable: false, // Backend doesn't support sorting by content_count sortable: false, // Backend doesn't support sorting by content_count
sortField: 'content_count', sortField: 'content_count',
width: '120px', width: '120px',
align: 'center' as const,
render: (value: number) => value.toLocaleString(), render: (value: number) => value.toLocaleString(),
}, },
{ {
@@ -244,6 +250,32 @@ export const createClustersPageConfig = (
defaultVisible: false, defaultVisible: false,
render: (value: string) => formatRelativeDate(value), render: (value: string) => formatRelativeDate(value),
}, },
// Generate Ideas action column - only shows button for status = 'new'
{
key: 'generate_action',
label: 'Actions',
sortable: false,
width: '120px',
render: (_value: any, row: Cluster) => {
// Only show generate button for clusters with status 'new'
if (row.status === 'new' && handlers.onGenerateIdeas) {
return (
<button
onClick={(e) => {
e.stopPropagation();
handlers.onGenerateIdeas!(row.id);
}}
className="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded transition-colors"
title="Generate Ideas"
>
<BoltIcon className="w-4 h-4" />
Generate
</button>
);
}
return <span className="text-gray-400 dark:text-gray-500 text-sm">-</span>;
},
},
], ],
filters: [ filters: [
{ {

View File

@@ -296,7 +296,7 @@ export const createContentPageConfig = (
sortable: true, sortable: true,
sortField: 'word_count', sortField: 'word_count',
width: '100px', width: '100px',
align: 'right', align: 'center' as const,
render: (value: number) => ( render: (value: number) => (
<span className="font-mono text-sm text-gray-700 dark:text-gray-300"> <span className="font-mono text-sm text-gray-700 dark:text-gray-300">
{value?.toLocaleString() || 0} {value?.toLocaleString() || 0}

View File

@@ -203,6 +203,7 @@ export const createIdeasPageConfig = (
sortable: true, sortable: true,
sortField: 'estimated_word_count', sortField: 'estimated_word_count',
width: '100px', width: '100px',
align: 'center' as const,
render: (value: number) => value.toLocaleString(), render: (value: number) => value.toLocaleString(),
}, },
{ {

View File

@@ -160,6 +160,7 @@ export const createKeywordsPageConfig = (
...volumeColumn, ...volumeColumn,
sortable: true, sortable: true,
sortField: 'seed_keyword__volume', // Backend expects seed_keyword__volume sortField: 'seed_keyword__volume', // Backend expects seed_keyword__volume
align: 'center' as const,
render: (value: number) => value.toLocaleString(), render: (value: number) => value.toLocaleString(),
}, },
{ {
@@ -200,6 +201,7 @@ export const createKeywordsPageConfig = (
...countryColumn, ...countryColumn,
sortable: false, // Backend doesn't support sorting by country sortable: false, // Backend doesn't support sorting by country
sortField: 'seed_keyword__country', sortField: 'seed_keyword__country',
align: 'center' as const,
render: (value: string) => { render: (value: string) => {
const countryNames: Record<string, string> = { const countryNames: Record<string, string> = {
'US': 'United States', 'US': 'United States',

View File

@@ -155,12 +155,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
icon: EditIcon, icon: EditIcon,
variant: 'primary', variant: 'primary',
}, },
{ // Generate Ideas moved to dedicated column - only shows for status='new'
key: 'generate_ideas',
label: 'Generate Ideas',
icon: <BoltIcon className="w-5 h-5" />,
variant: 'primary',
},
], ],
bulkActions: [ bulkActions: [
{ {
@@ -175,12 +170,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
icon: <DownloadIcon className="w-4 h-4 text-blue-light-500" />, icon: <DownloadIcon className="w-4 h-4 text-blue-light-500" />,
variant: 'secondary', variant: 'secondary',
}, },
{ // Removed auto_generate_ideas - now row-level only
key: 'auto_generate_ideas',
label: 'Generate Ideas',
icon: <BoltIcon className="w-4 h-4 text-warning-500" />,
variant: 'secondary',
},
], ],
}, },
'/planner/ideas': { '/planner/ideas': {

View File

@@ -215,6 +215,7 @@ export const createTasksPageConfig = (
...wordCountColumn, ...wordCountColumn,
sortable: true, sortable: true,
sortField: 'word_count', sortField: 'word_count',
align: 'center' as const,
render: (value: number | null | undefined) => (value != null ? value.toLocaleString() : '-'), render: (value: number | null | undefined) => (value != null ? value.toLocaleString() : '-'),
}, },
{ {

View File

@@ -452,16 +452,17 @@ const AppSidebar: React.FC = () => {
onMouseEnter={() => !isExpanded && setIsHovered(true)} onMouseEnter={() => !isExpanded && setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
{/* Collapse/Expand Toggle Button - Position changes based on state */} {/* Collapse/Expand Toggle Button - Fixed 5px from sidebar edge, does not move with hover */}
<button <button
onClick={toggleSidebar} 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-all duration-300 z-50 ${ 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 || isHovered ? '-right-3' : 'right-3' isExpanded ? 'left-[295px]' : 'left-[95px]'
}`} }`}
style={{ position: 'fixed' }}
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"} aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
> >
<svg <svg
className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-300 ${isExpanded || isHovered ? 'rotate-180' : ''}`} className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"

View File

@@ -331,6 +331,7 @@ export default function Clusters() {
volumeDropdownRef, volumeDropdownRef,
setCurrentPage, setCurrentPage,
loadClusters, loadClusters,
onGenerateIdeas: (clusterId: number) => handleRowAction('generate_ideas', { id: clusterId } as Cluster),
}); });
}, [ }, [
activeSector, activeSector,
@@ -344,6 +345,7 @@ export default function Clusters() {
tempVolumeMin, tempVolumeMin,
tempVolumeMax, tempVolumeMax,
loadClusters, loadClusters,
handleRowAction,
]); ]);
// Calculate header metrics // Calculate header metrics
@@ -405,12 +407,6 @@ export default function Clusters() {
volumeMin: volumeMin, volumeMin: volumeMin,
volumeMax: volumeMax, volumeMax: volumeMax,
}} }}
primaryAction={{
label: 'Generate Ideas',
icon: <BoltIcon className="w-4 h-4" />,
onClick: () => handleBulkAction('auto_generate_ideas', selectedIds),
variant: 'success',
}}
onFilterChange={(key, value) => { onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value); const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') { if (key === 'search') {

View File

@@ -4,6 +4,7 @@
*/ */
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate'; import TablePageTemplate from '../../templates/TablePageTemplate';
import { import {
fetchContent, fetchContent,
@@ -14,9 +15,8 @@ import {
bulkDeleteContent, bulkDeleteContent,
} from '../../services/api'; } from '../../services/api';
import { optimizerApi } from '../../api/optimizer.api'; import { optimizerApi } from '../../api/optimizer.api';
import { useNavigate } from 'react-router-dom';
import { useToast } from '../../components/ui/toast/ToastContainer'; import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, TaskIcon, CheckCircleIcon } from '../../icons'; import { FileIcon, TaskIcon, CheckCircleIcon, ArrowRightIcon } from '../../icons';
import { createContentPageConfig } from '../../config/pages/content.config'; import { createContentPageConfig } from '../../config/pages/content.config';
import { useSectorStore } from '../../store/sectorStore'; import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore'; import { usePageSizeStore } from '../../store/pageSizeStore';
@@ -273,6 +273,28 @@ export default function Content() {
onDelete={handleDelete} onDelete={handleDelete}
onBulkDelete={handleBulkDelete} onBulkDelete={handleBulkDelete}
getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`} 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>
}
/> />
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}

View File

@@ -4,6 +4,7 @@
*/ */
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { Link } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate'; import TablePageTemplate from '../../templates/TablePageTemplate';
import { import {
fetchContentImages, fetchContentImages,
@@ -18,7 +19,7 @@ import {
bulkDeleteContent, bulkDeleteContent,
} from '../../services/api'; } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer'; import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon } from '../../icons'; import { FileIcon, DownloadIcon, ArrowRightIcon } from '../../icons';
import { PhotoIcon } from '@heroicons/react/24/outline'; import { PhotoIcon } from '@heroicons/react/24/outline';
import { createImagesPageConfig } from '../../config/pages/images.config'; import { createImagesPageConfig } from '../../config/pages/images.config';
import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal'; import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal';
@@ -514,6 +515,28 @@ export default function Images() {
setCurrentPage(1); setCurrentPage(1);
}} }}
onRowAction={handleRowAction} 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>
}
/> />
<ImageQueueModal <ImageQueueModal
isOpen={isQueueModalOpen} isOpen={isQueueModalOpen}

View File

@@ -453,6 +453,13 @@ export default function Review() {
setCurrentPage(1); setCurrentPage(1);
}} }}
onRowAction={handleRowAction} 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>
}
/> />
<ModuleMetricsFooter <ModuleMetricsFooter
metrics={[ metrics={[

View File

@@ -3,7 +3,7 @@
* Consistent with Keywords page layout, structure and design * Consistent with Keywords page layout, structure and design
*/ */
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate'; import TablePageTemplate from '../../templates/TablePageTemplate';
import { import {
fetchTasks, fetchTasks,

View File

@@ -164,6 +164,8 @@ interface TablePageTemplateProps {
}; };
// Custom row highlight function (returns bg class based on row data) // Custom row highlight function (returns bg class based on row data)
getRowClassName?: (row: any) => string; getRowClassName?: (row: any) => string;
// Status explainer component to display on right side of table actions row
statusExplainer?: ReactNode;
} }
export default function TablePageTemplate({ export default function TablePageTemplate({
@@ -202,6 +204,7 @@ export default function TablePageTemplate({
bulkActions: customBulkActions, bulkActions: customBulkActions,
primaryAction, primaryAction,
getRowClassName, getRowClassName,
statusExplainer,
}: TablePageTemplateProps) { }: TablePageTemplateProps) {
const location = useLocation(); const location = useLocation();
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false); const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
@@ -752,7 +755,16 @@ export default function TablePageTemplate({
</div> </div>
)} )}
{/* Right side - Action Buttons */} {/* Right side - Status Explainer and Action Buttons */}
<div className="flex gap-4 items-start">
{/* Status Explainer */}
{statusExplainer && (
<div className="text-right">
{statusExplainer}
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* Custom Actions */} {/* Custom Actions */}
{customActions} {customActions}
@@ -788,6 +800,7 @@ export default function TablePageTemplate({
)} )}
</div> </div>
</div> </div>
</div>
{/* Data Table - Match Keywords.tsx exact styling */} {/* Data Table - Match Keywords.tsx exact styling */}
<div className={`rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03] igny8-table-container ${!showContent ? 'loading' : 'loaded'}`} style={{ overflowX: 'auto', overflowY: 'visible' }}> <div className={`rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03] igny8-table-container ${!showContent ? 'loading' : 'loaded'}`} style={{ overflowX: 'auto', overflowY: 'visible' }}>