From efd7193951ecae8ea552e7ef90c9644019a17144 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 27 Dec 2025 08:56:09 +0000 Subject: [PATCH] more fixes --- .../components/common/StatusMetricsCard.tsx | 183 -------- frontend/src/layout/AppLayout.tsx | 23 +- frontend/src/pages/Writer/Content.tsx | 35 -- frontend/src/pages/Writer/Images.tsx | 36 -- frontend/src/pages/Writer/Review.tsx | 14 - frontend/src/pages/Writer/Tasks.tsx | 39 -- frontend/src/styles/igny8-colors.css | 10 +- frontend/src/templates/TablePageTemplate.tsx | 400 +++++++++--------- 8 files changed, 217 insertions(+), 523 deletions(-) delete mode 100644 frontend/src/components/common/StatusMetricsCard.tsx diff --git a/frontend/src/components/common/StatusMetricsCard.tsx b/frontend/src/components/common/StatusMetricsCard.tsx deleted file mode 100644 index 854ea62c..00000000 --- a/frontend/src/components/common/StatusMetricsCard.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/** - * 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 = { - 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 ( -
-
- {/* Icon */} - {icon && ( -
-
- {icon} -
-
- )} - - {/* Content */} -
- {/* Title and Count Row */} -
-

{title}

- {count} -
- - {/* Subtitle */} - {subtitle && ( -

{subtitle}

- )} - - {/* 2-column metrics */} - {metrics && metrics.length > 0 && ( -
- {metrics.map((metric, index) => ( -
- {metric.label} - {metric.value} -
- ))} -
- )} - - {/* Review link/button */} - {(reviewCount !== undefined || actionButton) && ( -
- {reviewCount !== undefined && ( - - {reviewCount} awaiting review - - )} - {actionButton && ( - - {actionButton.label} - - - - - )} -
- )} -
-
-
- ); -} diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx index ce1b4f4b..53a0b91f 100644 --- a/frontend/src/layout/AppLayout.tsx +++ b/frontend/src/layout/AppLayout.tsx @@ -139,10 +139,23 @@ const LayoutContent: React.FC = () => { accentColor = 'purple'; } - // Set content balance - show as "X/Y Content" for simplicity + // Format credit value with K/M suffix for large numbers + const formatCredits = (val: number): string => { + if (val >= 1000000) { + const millions = val / 1000000; + return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`; + } + if (val >= 1000) { + const thousands = val / 1000; + return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`; + } + return val.toString(); + }; + + // Set credit balance - show as "used/total Credits" setMetrics([{ - label: 'Content', - value: total > 0 ? `${remaining}/${total}` : remaining, + label: 'Credits', + value: total > 0 ? `${formatCredits(remaining)}/${formatCredits(total)}` : formatCredits(remaining), accentColor, }]); }, [balance, isAuthenticated, setMetrics]); @@ -160,8 +173,8 @@ const LayoutContent: React.FC = () => { > {/* Pending Payment Banner - Shows when account status is 'pending_payment' */} - -
+ +
diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index f0f2f22d..56318c68 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -25,7 +25,6 @@ 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(); @@ -56,22 +55,6 @@ 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); @@ -290,24 +273,6 @@ export default function Content() { onDelete={handleDelete} onBulkDelete={handleBulkDelete} getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`} - statusExplainer={ - } - 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', - }} - /> - } /> {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 90e793ba..5371a987 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -17,7 +17,6 @@ import { fetchAPI, deleteContent, bulkDeleteContent, - fetchContent, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, ArrowRightIcon } from '../../icons'; @@ -27,7 +26,6 @@ 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(); @@ -70,22 +68,6 @@ export default function Images() { const [isImageModalOpen, setIsImageModalOpen] = useState(false); const [modalImageUrl, setModalImageUrl] = useState(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); @@ -533,24 +515,6 @@ export default function Images() { setCurrentPage(1); }} onRowAction={handleRowAction} - statusExplainer={ - } - 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', - }} - /> - } /> } - count={totalCount} - subtitle="awaiting approval" - actionButton={{ - label: 'Approved', - href: '/writer/approved', - }} - /> - } /> (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 @@ -486,24 +465,6 @@ export default function Tasks() { setTypeFilter(''); setCurrentPage(1); }} - statusExplainer={ - } - 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 */} diff --git a/frontend/src/styles/igny8-colors.css b/frontend/src/styles/igny8-colors.css index dcfba0d3..a83f0d57 100644 --- a/frontend/src/styles/igny8-colors.css +++ b/frontend/src/styles/igny8-colors.css @@ -240,7 +240,7 @@ text-align: left !important; background-color: #f8fafc !important; /* Light gray background */ border-bottom: 2px solid #e2e8f0 !important; /* Thicker bottom border */ - text-transform: uppercase; + text-transform: capitalize; letter-spacing: 0.3px; } @@ -365,10 +365,10 @@ select.igny8-select-styled option:checked { } .igny8-header-metric-label { - font-size: 10px; + font-size: 13px; /* increased from 10px by 25%+ */ font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; + text-transform: capitalize; + letter-spacing: 0.3px; color: rgb(100 116 139); /* slate-500 */ } @@ -377,7 +377,7 @@ select.igny8-select-styled option:checked { } .igny8-header-metric-value { - font-size: 14px; + font-size: 16px; /* increased from 14px */ font-weight: 700; color: rgb(30 41 59); /* slate-800 */ margin-left: 4px; diff --git a/frontend/src/templates/TablePageTemplate.tsx b/frontend/src/templates/TablePageTemplate.tsx index 3cd91bac..5fb641d9 100644 --- a/frontend/src/templates/TablePageTemplate.tsx +++ b/frontend/src/templates/TablePageTemplate.tsx @@ -34,6 +34,20 @@ const formatColumnKey = (key: string): string => { .join(' '); }; +// Helper function to format metric values with K/M suffixes +// 3 digits show actual, 4+ digits show K, 1M+ show M +const formatMetricValue = (value: number): string => { + if (value >= 1000000) { + const millions = value / 1000000; + return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`; + } + if (value >= 1000) { + const thousands = value / 1000; + return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`; + } + return value.toString(); +}; + import Checkbox from '../components/form/input/Checkbox'; import Button from '../components/ui/button/Button'; import Input from '../components/form/input/InputField'; @@ -41,9 +55,8 @@ import SelectDropdown from '../components/form/SelectDropdown'; import { Dropdown } from '../components/ui/dropdown/Dropdown'; import { DropdownItem } from '../components/ui/dropdown/DropdownItem'; import AlertModal from '../components/ui/alert/AlertModal'; -import { ChevronDownIcon, MoreDotIcon, PlusIcon } from '../icons'; +import { ChevronDownIcon, MoreDotIcon, PlusIcon, InfoIcon } from '../icons'; import { FunnelIcon } from '@heroicons/react/24/outline'; -import { useHeaderMetrics } from '../context/HeaderMetricsContext'; import { useToast } from '../components/ui/toast/ToastContainer'; import { getDeleteModalConfig } from '../config/pages/delete-modal.config'; import { getBulkActionModalConfig } from '../config/pages/bulk-action-modal.config'; @@ -55,6 +68,7 @@ import { usePageSizeStore } from '../store/pageSizeStore'; import { useColumnVisibilityStore } from '../store/columnVisibilityStore'; import ToggleTableRow, { ToggleButton } from '../components/common/ToggleTableRow'; import ColumnSelector from '../components/common/ColumnSelector'; +import { Tooltip } from '../components/ui/tooltip/Tooltip'; interface ColumnConfig { key: string; @@ -94,6 +108,7 @@ interface HeaderMetrics { label: string; value: string | number; accentColor: 'blue' | 'green' | 'amber' | 'purple'; + tooltip?: string; } interface TablePageTemplateProps { @@ -164,8 +179,6 @@ interface TablePageTemplateProps { }; // Custom row highlight function (returns bg class based on row data) getRowClassName?: (row: any) => string; - // Status explainer component to display on right side of table actions row - statusExplainer?: ReactNode; } export default function TablePageTemplate({ @@ -204,7 +217,6 @@ export default function TablePageTemplate({ bulkActions: customBulkActions, primaryAction, getRowClassName, - statusExplainer, }: TablePageTemplateProps) { const location = useLocation(); const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false); @@ -264,7 +276,6 @@ export default function TablePageTemplate({ isLoading: false, }); - const { setMetrics } = useHeaderMetrics(); const toast = useToast(); const { pageSize, setPageSize } = usePageSizeStore(); const { getVisibleColumns, setVisibleColumns: saveVisibleColumns } = useColumnVisibilityStore(); @@ -443,50 +454,6 @@ export default function TablePageTemplate({ } }; - // Set header metrics when provided - // Use a ref to track previous metrics and only update when values actually change - const prevMetricsRef = useRef(''); - const hasSetMetricsRef = useRef(false); - - // Create a stable key for comparison - only when headerMetrics has values - const metricsKey = useMemo(() => { - if (!headerMetrics || headerMetrics.length === 0) return ''; - try { - return headerMetrics.map(m => `${m.label}:${String(m.value)}`).join('|'); - } catch { - return ''; - } - }, [headerMetrics]); - - useEffect(() => { - // Skip if metrics haven't actually changed - if (metricsKey === prevMetricsRef.current) { - return; - } - - // Update metrics if we have new values - // HeaderMetricsContext will automatically merge these with credit balance - if (metricsKey) { - setMetrics(headerMetrics); - hasSetMetricsRef.current = true; - prevMetricsRef.current = metricsKey; - } else if (hasSetMetricsRef.current) { - // Clear page metrics (credit balance will be preserved by HeaderMetricsContext) - setMetrics([]); - hasSetMetricsRef.current = false; - prevMetricsRef.current = ''; - } - - // Cleanup: clear page metrics when component unmounts (credit balance preserved) - return () => { - if (hasSetMetricsRef.current) { - setMetrics([]); - hasSetMetricsRef.current = false; - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricsKey]); // Only depend on the stable key - // Check if any filters are applied // When using renderFilters, check filterValues directly; otherwise check filters prop const hasActiveFilters = (renderFilters || filters.length > 0) && Object.values(filterValues).some((value) => { @@ -577,124 +544,193 @@ export default function TablePageTemplate({ return (
- {/* Bulk Actions and Action Buttons Row - Fixed height container */} -
- {/* Left side - Primary Action, Bulk Actions, and Filter Toggle */} -
- {/* Primary Workflow Action Button - Only enabled with selection */} - {primaryAction && ( - + )} + + {/* Primary Workflow Action Button - Only shown with selection */} + {primaryAction && selectedIds.length > 0 && ( + - )} - - {/* Bulk Actions - Single button if only one action, dropdown if multiple */} - {bulkActions.length > 0 && ( -
- {bulkActions.length === 1 ? ( - // Single button for single action - + )} + + {/* Bulk Actions - Only shown with selection */} + {bulkActions.length > 0 && selectedIds.length > 0 && ( +
+ {bulkActions.length === 1 ? ( + - ) : ( - // Dropdown for multiple actions - <> - + ) : ( + <> + - 0} - onClose={() => setIsBulkActionsDropdownOpen(false)} - anchorRef={bulkActionsButtonRef as React.RefObject} - placement="bottom-left" - className="w-48 p-2" - > - {bulkActions.map((action, index) => { - const isDelete = action.key === 'delete'; - const showDivider = isDelete && index > 0; - return ( - - {showDivider &&
} - { - handleBulkActionClick(action.key, selectedIds); - }} - className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ - isDelete - ? "text-error-500 hover:bg-error-50 hover:text-error-600 dark:text-error-400 dark:hover:bg-error-500/15 dark:hover:text-error-300" - : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" - }`} - > - {action.icon && {action.icon}} - {action.label} - -
- ); - })} -
- + + setIsBulkActionsDropdownOpen(false)} + anchorRef={bulkActionsButtonRef as React.RefObject} + placement="bottom-left" + className="w-48 p-2" + > + {bulkActions.map((action, index) => { + const isDelete = action.key === 'delete'; + const showDivider = isDelete && index > 0; + return ( + + {showDivider &&
} + { + handleBulkActionClick(action.key, selectedIds); + }} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + isDelete + ? "text-error-500 hover:bg-error-50 hover:text-error-600 dark:text-error-400 dark:hover:bg-error-500/15 dark:hover:text-error-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + {action.icon && {action.icon}} + {action.label} + +
+ ); + })} +
+ + )} +
+ )} + + {/* Filter Toggle Button */} + {(renderFilters || filters.length > 0) && ( + + )} +
+ + {/* Right side - Inline Metrics and Action Buttons */} +
+ {/* Inline Metrics - with 25% larger label fonts */} + {headerMetrics && headerMetrics.length > 0 && ( +
+ {headerMetrics.map((metric, index) => { + const metricElement = ( +
+
+ + {metric.label} + {metric.tooltip && ( + + )} + + + {typeof metric.value === 'number' ? formatMetricValue(metric.value) : metric.value} + +
+ ); + + return ( + + {metric.tooltip ? ( + + {metricElement} + + ) : ( + metricElement + )} + {index < headerMetrics.length - 1 && ( +
+ )} +
+ ); + })} +
+ )} + + {/* Action Buttons */} +
+ {/* Custom Actions */} + {customActions} + {onExportCSV && ( + + )} + {onImport && ( + )}
- )} - - {/* Filter Toggle Button */} - {(renderFilters || filters.length > 0) && ( - - )} +
- {/* Center - Filters (when toggled on) */} + {/* Filters Row - Below action buttons, centered */} {showFilters && (renderFilters || filters.length > 0) && ( -
+
{renderFilters ? ( @@ -702,7 +738,6 @@ export default function TablePageTemplate({ ) : ( <> {filters.map((filter) => { - // Handle custom render filters (for complex filters like volume range) if (filter.type === 'custom' && (filter as any).customRender) { return {(filter as any).customRender()}; } @@ -717,7 +752,7 @@ export default function TablePageTemplate({ onChange={(e) => { onFilterChange?.(filter.key, e.target.value); }} - className="w-full sm:flex-1 h-9" + className="w-full sm:flex-1 h-8" /> ); } else if (filter.type === 'select') { @@ -729,7 +764,6 @@ export default function TablePageTemplate({ placeholder={filter.label} value={currentValue} onChange={(value) => { - // Ensure we pass the value even if it's an empty string const newValue = value === null || value === undefined ? '' : String(value); onFilterChange?.(filter.key, newValue); }} @@ -754,52 +788,6 @@ export default function TablePageTemplate({
)} - - {/* Right side - Status Explainer and Action Buttons */} -
- {/* Status Explainer */} - {statusExplainer && ( -
- {statusExplainer} -
- )} - - {/* Action Buttons */} -
- {/* Custom Actions */} - {customActions} - {onExportCSV && ( - - )} - {onImport && ( - - )} - {onCreate && ( - - )} -
-
{/* Data Table - Match Keywords.tsx exact styling */}