page adn app header mods

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-27 04:09:05 +00:00
parent e5959c3e72
commit fd6e7eb2dd
14 changed files with 494 additions and 547 deletions

View File

@@ -4,6 +4,7 @@
*/
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { Link } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchClusters,
@@ -28,7 +29,6 @@ import { usePageSizeStore } from '../../store/pageSizeStore';
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
import PageHeader from '../../components/common/PageHeader';
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
export default function Clusters() {
const toast = useToast();
@@ -76,61 +76,6 @@ export default function Clusters() {
const progressModal = useProgressModal();
const hasReloadedRef = useRef(false);
// Calculate workflow insights
const workflowInsights: WorkflowInsight[] = useMemo(() => {
const insights: WorkflowInsight[] = [];
const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length;
const totalIdeas = clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0);
const emptyClusters = clusters.filter(c => (c.keywords_count || 0) === 0).length;
const thinClusters = clusters.filter(c => (c.keywords_count || 0) > 0 && (c.keywords_count || 0) < 3).length;
const readyForGeneration = clustersWithIdeas;
const generationRate = totalCount > 0 ? Math.round((readyForGeneration / totalCount) * 100) : 0;
if (totalCount === 0) {
insights.push({
type: 'info',
message: 'Create clusters to organize keywords into topical groups for better content planning',
});
return insights;
}
// Content generation readiness
if (generationRate < 30) {
insights.push({
type: 'warning',
message: `Only ${generationRate}% of clusters have content ideas - Generate ideas to unlock content pipeline`,
});
} else if (generationRate >= 70) {
insights.push({
type: 'success',
message: `${generationRate}% of clusters have ideas (${totalIdeas} total) - Strong content pipeline ready`,
});
}
// Empty or thin clusters
if (emptyClusters > 0) {
insights.push({
type: 'warning',
message: `${emptyClusters} clusters have no keywords - Map keywords or delete unused clusters`,
});
} else if (thinClusters > 2) {
insights.push({
type: 'info',
message: `${thinClusters} clusters have fewer than 3 keywords - Consider adding more related keywords for better coverage`,
});
}
// Actionable next step
if (totalIdeas === 0) {
insights.push({
type: 'action',
message: 'Select clusters and use Auto-Generate Ideas to create content briefs',
});
}
return insights;
}, [clusters, totalCount]);
// Load clusters - wrapped in useCallback to prevent infinite loops
const loadClusters = useCallback(async () => {
setLoading(true);
@@ -445,10 +390,20 @@ export default function Clusters() {
<>
<PageHeader
title="Clusters"
description="Keyword groups organized by topic. Generate content ideas from clusters to build topical authority."
description="Group keywords into topic clusters"
badge={{ icon: <GroupIcon />, color: 'purple' }}
breadcrumb="Planner / Clusters"
workflowInsights={workflowInsights}
breadcrumb="Planner"
actions={
<Link
to="/planner/ideas"
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
>
Generate Ideas
<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>
}
/>
<TablePageTemplate
columns={pageConfig.columns}

View File

@@ -4,6 +4,7 @@
*/
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Link } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContentIdeas,
@@ -24,12 +25,12 @@ 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 { LightBulbIcon } from '@heroicons/react/24/outline';
import { createIdeasPageConfig } from '../../config/pages/ideas.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader';
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
import { WorkflowInsight } from '../../components/common/WorkflowInsights';
export default function Ideas() {
const toast = useToast();
@@ -77,57 +78,6 @@ export default function Ideas() {
// Progress modal for AI functions
const progressModal = useProgressModal();
// Calculate workflow insights
const workflowInsights: WorkflowInsight[] = useMemo(() => {
const insights: WorkflowInsight[] = [];
const newCount = ideas.filter(i => i.status === 'new').length;
const queuedCount = ideas.filter(i => i.status === 'queued').length;
const completedCount = ideas.filter(i => i.status === 'completed').length;
const queueActivationRate = totalCount > 0 ? Math.round((queuedCount / totalCount) * 100) : 0;
const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
if (totalCount === 0) {
insights.push({
type: 'info',
message: 'Generate ideas from your keyword clusters to build your content pipeline',
});
return insights;
}
// Queue activation insights
if (newCount > 0 && queuedCount === 0) {
insights.push({
type: 'warning',
message: `${newCount} new ideas waiting - Queue them to activate the content pipeline`,
});
} else if (queueActivationRate > 0 && queueActivationRate < 40) {
insights.push({
type: 'info',
message: `${queueActivationRate}% of ideas queued (${queuedCount} ideas) - Queue more ideas to maintain steady content flow`,
});
} else if (queuedCount > 0) {
insights.push({
type: 'success',
message: `${queuedCount} ideas in queue - Content pipeline is active and ready for task generation`,
});
}
// Completion velocity
if (completionRate >= 50) {
insights.push({
type: 'success',
message: `Strong completion rate (${completionRate}%) - ${completedCount} ideas converted to content`,
});
} else if (completionRate > 0) {
insights.push({
type: 'info',
message: `${completedCount} ideas completed (${completionRate}%) - Continue queuing ideas to grow content library`,
});
}
return insights;
}, [ideas, totalCount]);
// Load clusters for filter dropdown
useEffect(() => {
const loadClusters = async () => {
@@ -351,10 +301,20 @@ export default function Ideas() {
<>
<PageHeader
title="Ideas"
description="AI-generated content ideas with titles, outlines, and target keywords. Queue ideas to start content generation."
badge={{ icon: <BoltIcon />, color: 'orange' }}
breadcrumb="Planner / Ideas"
workflowInsights={workflowInsights}
description="Content ideas generated from keywords"
badge={{ icon: <LightBulbIcon />, color: 'yellow' }}
breadcrumb="Planner"
actions={
<Link
to="/writer/queue"
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
>
Start Writing
<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>
}
/>
<TablePageTemplate
columns={pageConfig.columns}

View File

@@ -5,6 +5,7 @@
*/
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Link } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchKeywords,
@@ -346,6 +347,18 @@ export default function Keywords() {
}
}, [toast, activeSector, loadKeywords, progressModal, keywords]);
// Quick auto-cluster unclustered keywords (for Next Step button)
const handleAutoCluster = useCallback(async () => {
const unclusteredIds = keywords.filter(k => !k.cluster_id).map(k => k.id);
if (unclusteredIds.length === 0) {
toast.info('All keywords are already clustered');
return;
}
// Limit to 50 keywords
const idsToCluster = unclusteredIds.slice(0, 50);
await handleBulkAction('auto_cluster', idsToCluster.map(String));
}, [keywords, handleBulkAction, toast]);
// Reset reload flag when modal closes or opens
useEffect(() => {
if (!progressModal.isOpen) {
@@ -481,63 +494,32 @@ export default function Keywords() {
}, [pageConfig?.headerMetrics, keywords, totalCount, clusters]);
// Calculate workflow insights based on UX doc principles
const workflowInsights = useMemo(() => {
const insights = [];
const workflowStats = useMemo(() => {
const clusteredCount = keywords.filter(k => k.cluster_id).length;
const unclusteredCount = totalCount - clusteredCount;
const pipelineReadiness = totalCount > 0 ? Math.round((clusteredCount / totalCount) * 100) : 0;
return {
total: totalCount,
clustered: clusteredCount,
unclustered: unclusteredCount,
readiness: pipelineReadiness,
};
}, [keywords, totalCount]);
// Determine next step action
const nextStep = useMemo(() => {
if (totalCount === 0) {
insights.push({
type: 'info' as const,
message: 'Import keywords to begin building your content strategy and unlock SEO opportunities',
});
return insights;
return { label: 'Import Keywords', path: '/add-keywords', disabled: false };
}
// Pipeline Readiness Score insight
if (pipelineReadiness < 30) {
insights.push({
type: 'warning' as const,
message: `Pipeline readiness at ${pipelineReadiness}% - Most keywords need clustering before content ideation can begin`,
});
} else if (pipelineReadiness < 60) {
insights.push({
type: 'info' as const,
message: `Pipeline readiness at ${pipelineReadiness}% - Clustering progress is moderate, continue organizing keywords`,
});
} else if (pipelineReadiness >= 85) {
insights.push({
type: 'success' as const,
message: `Excellent pipeline readiness (${pipelineReadiness}%) - Ready for content ideation phase`,
});
if (workflowStats.unclustered >= 5) {
return { label: 'Auto-Cluster', action: 'cluster', disabled: false };
}
// Clustering Potential (minimum batch size check)
if (unclusteredCount >= 5) {
insights.push({
type: 'action' as const,
message: `${unclusteredCount} keywords available for auto-clustering (minimum batch size met)`,
});
} else if (unclusteredCount > 0 && unclusteredCount < 5) {
insights.push({
type: 'info' as const,
message: `${unclusteredCount} unclustered keywords - Need ${5 - unclusteredCount} more to run auto-cluster`,
});
if (workflowStats.clustered > 0) {
return { label: 'Generate Ideas', path: '/planner/ideas', disabled: false };
}
// Coverage Gaps - thin clusters that need more research
const thinClusters = clusters.filter(c => (c.keywords_count || 0) === 1);
if (thinClusters.length > 3) {
const thinVolume = thinClusters.reduce((sum, c) => sum + (c.volume || 0), 0);
insights.push({
type: 'warning' as const,
message: `${thinClusters.length} clusters have only 1 keyword each (${thinVolume.toLocaleString()} monthly volume) - Consider expanding research`,
});
}
return insights;
}, [keywords, totalCount, clusters]);
return { label: 'Add More Keywords', path: '/add-keywords', disabled: false };
}, [totalCount, workflowStats]);
// Handle create/edit
const handleSave = async () => {
@@ -612,10 +594,37 @@ export default function Keywords() {
<>
<PageHeader
title="Keywords"
description="Your target search terms organized for content creation. Import, cluster, and transform into content ideas."
description="Your target search terms organized for content creation"
badge={{ icon: <ListIcon />, color: 'green' }}
breadcrumb="Planner / Keywords"
workflowInsights={workflowInsights}
breadcrumb="Planner"
actions={
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500 dark:text-gray-400 hidden md:block">
{workflowStats.clustered}/{workflowStats.total} clustered
</span>
{nextStep.path ? (
<Link
to={nextStep.path}
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
>
{nextStep.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>
) : nextStep.action === 'cluster' ? (
<button
onClick={handleAutoCluster}
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors"
>
{nextStep.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>
</button>
) : null}
</div>
}
/>
<TablePageTemplate
columns={pageConfig.columns}