Files
igny8/frontend/src/pages/Planner/Dashboard.tsx
alorig f255e3c0a0 21
2025-11-24 06:08:27 +05:00

780 lines
30 KiB
TypeScript

import { useEffect, useState, useMemo, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
import PageHeader from "../../components/common/PageHeader";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
ListIcon,
GroupIcon,
BoltIcon,
PieChartIcon,
ArrowRightIcon,
CheckCircleIcon,
TimeIcon,
ArrowUpIcon,
ArrowDownIcon,
PlugInIcon,
ClockIcon,
} from "../../icons";
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
fetchSiteBlueprints,
SiteBlueprint,
} from "../../services/api";
import { useSiteStore } from "../../store/siteStore";
import { useSectorStore } from "../../store/sectorStore";
interface DashboardStats {
keywords: {
total: number;
mapped: number;
unmapped: number;
byStatus: Record<string, number>;
byIntent: Record<string, number>;
};
clusters: {
total: number;
withIdeas: number;
withoutIdeas: number;
totalVolume: number;
avgKeywords: number;
topClusters: Array<{ id: number; name: string; volume: number; keywords_count: number }>;
};
ideas: {
total: number;
queued: number;
notQueued: number;
byStatus: Record<string, number>;
byContentType: Record<string, number>;
};
workflow: {
keywordsReady: boolean;
clustersBuilt: boolean;
ideasGenerated: boolean;
readyForWriter: boolean;
};
}
export default function PlannerDashboard() {
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Fetch real data
const fetchDashboardData = async () => {
try {
setLoading(true);
const [keywordsRes, clustersRes, ideasRes, tasksRes, blueprintsRes] = await Promise.all([
fetchKeywords({ page_size: 1000, sector_id: activeSector?.id }),
fetchClusters({ page_size: 1000, sector_id: activeSector?.id }),
fetchContentIdeas({ page_size: 1000, sector_id: activeSector?.id }),
fetchTasks({ page_size: 1000, sector_id: activeSector?.id }),
activeSite?.id ? fetchSiteBlueprints({ site_id: activeSite.id, page_size: 100 }) : Promise.resolve({ results: [] })
]);
const keywords = keywordsRes.results || [];
const mappedKeywords = keywords.filter(k => k.cluster && k.cluster.length > 0);
const unmappedKeywords = keywords.filter(k => !k.cluster || k.cluster.length === 0);
const keywordsByStatus: Record<string, number> = {};
const keywordsByIntent: Record<string, number> = {};
keywords.forEach(k => {
keywordsByStatus[k.status || 'unknown'] = (keywordsByStatus[k.status || 'unknown'] || 0) + 1;
if (k.intent) {
keywordsByIntent[k.intent] = (keywordsByIntent[k.intent] || 0) + 1;
}
});
const clusters = clustersRes.results || [];
const clustersWithIdeas = clusters.filter(c => c.keywords_count > 0);
const totalVolume = clusters.reduce((sum, c) => sum + (c.volume || 0), 0);
const totalKeywordsInClusters = clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0);
const avgKeywords = clusters.length > 0 ? Math.round(totalKeywordsInClusters / clusters.length) : 0;
const topClusters = [...clusters]
.sort((a, b) => (b.volume || 0) - (a.volume || 0))
.slice(0, 5)
.map(c => ({
id: c.id,
name: c.name || 'Unnamed Cluster',
volume: c.volume || 0,
keywords_count: c.keywords_count || 0
}));
const ideas = ideasRes.results || [];
const ideaIds = new Set(ideas.map(i => i.id));
const tasks = tasksRes.results || [];
const queuedIdeas = tasks.filter(t => t.idea && ideaIds.has(t.idea)).length;
const notQueuedIdeas = ideas.length - queuedIdeas;
const ideasByStatus: Record<string, number> = {};
const ideasByContentType: Record<string, number> = {};
ideas.forEach(i => {
ideasByStatus[i.status || 'new'] = (ideasByStatus[i.status || 'new'] || 0) + 1;
if (i.content_type) {
ideasByContentType[i.content_type] = (ideasByContentType[i.content_type] || 0) + 1;
}
});
setStats({
keywords: {
total: keywords.length,
mapped: mappedKeywords.length,
unmapped: unmappedKeywords.length,
byStatus: keywordsByStatus,
byIntent: keywordsByIntent
},
clusters: {
total: clusters.length,
withIdeas: clustersWithIdeas.length,
withoutIdeas: clusters.length - clustersWithIdeas.length,
totalVolume,
avgKeywords,
topClusters
},
ideas: {
total: ideas.length,
queued: queuedIdeas,
notQueued: notQueuedIdeas,
byStatus: ideasByStatus,
byContentType: ideasByContentType
},
workflow: {
keywordsReady: keywords.length > 0,
clustersBuilt: clusters.length > 0,
ideasGenerated: ideas.length > 0,
readyForWriter: queuedIdeas > 0
}
});
setLastUpdated(new Date());
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
const interval = setInterval(fetchDashboardData, 30000);
return () => clearInterval(interval);
}, [activeSector?.id, activeSite?.id]);
const keywordMappingPct = useMemo(() => {
if (!stats || stats.keywords.total === 0) return 0;
return Math.round((stats.keywords.mapped / stats.keywords.total) * 100);
}, [stats]);
const clustersIdeasPct = useMemo(() => {
if (!stats || stats.clusters.total === 0) return 0;
return Math.round((stats.clusters.withIdeas / stats.clusters.total) * 100);
}, [stats]);
const ideasQueuedPct = useMemo(() => {
if (!stats || stats.ideas.total === 0) return 0;
return Math.round((stats.ideas.queued / stats.ideas.total) * 100);
}, [stats]);
const plannerModules = [
{
title: "Keywords",
description: "Manage and discover keywords",
icon: ListIcon,
color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
path: "/planner/keywords",
count: stats?.keywords.total || 0,
metric: `${stats?.keywords.mapped || 0} mapped`,
},
{
title: "Clusters",
description: "Keyword clusters and groups",
icon: GroupIcon,
color: "from-[var(--color-success)] to-[var(--color-success-dark)]",
path: "/planner/clusters",
count: stats?.clusters.total || 0,
metric: `${stats?.clusters.totalVolume.toLocaleString() || 0} volume`,
},
{
title: "Ideas",
description: "Content ideas and concepts",
icon: BoltIcon,
color: "from-[var(--color-warning)] to-[var(--color-warning-dark)]",
path: "/planner/ideas",
count: stats?.ideas.total || 0,
metric: `${stats?.ideas.queued || 0} queued`,
},
{
title: "Keyword Opportunities",
description: "Discover new keyword opportunities",
icon: PieChartIcon,
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
path: "/planner/keyword-opportunities",
count: 0,
metric: "Discover new keywords",
},
];
const recentActivity = [
{
id: 1,
type: "Keywords Clustered",
description: `${stats?.clusters.total || 0} new clusters created`,
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
icon: GroupIcon,
color: "text-green-600",
},
{
id: 2,
type: "Ideas Generated",
description: `${stats?.ideas.total || 0} content ideas created`,
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
icon: BoltIcon,
color: "text-orange-600",
},
{
id: 3,
type: "Keywords Added",
description: `${stats?.keywords.total || 0} keywords in database`,
timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000),
icon: ListIcon,
color: "text-blue-600",
},
];
const chartOptions: ApexOptions = {
chart: {
type: "area",
height: 300,
toolbar: { show: false },
zoom: { enabled: false },
},
stroke: {
curve: "smooth",
width: 3,
},
xaxis: {
categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
labels: { style: { colors: "#6b7280" } },
},
yaxis: {
labels: { style: { colors: "#6b7280" } },
},
legend: {
position: "top",
labels: { colors: "#6b7280" },
},
colors: ["var(--color-primary)", "var(--color-success)", "var(--color-warning)"],
grid: {
borderColor: "#e5e7eb",
},
fill: {
type: "gradient",
gradient: {
opacityFrom: 0.6,
opacityTo: 0.1,
},
},
};
const chartSeries = [
{
name: "Keywords Added",
data: [12, 19, 15, 25, 22, 18, 24],
},
{
name: "Clusters Created",
data: [8, 12, 10, 15, 14, 11, 16],
},
{
name: "Ideas Generated",
data: [5, 8, 6, 10, 9, 7, 11],
},
];
const keywordsStatusChart = useMemo(() => {
if (!stats) return null;
const options: ApexOptions = {
chart: {
type: 'donut',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false }
},
labels: Object.keys(stats.keywords.byStatus).filter(key => stats.keywords.byStatus[key] > 0),
colors: ['#465FFF', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'],
legend: {
position: 'bottom',
fontFamily: 'Outfit',
show: true
},
dataLabels: {
enabled: false
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: { show: false },
value: {
show: true,
fontSize: '24px',
fontWeight: 700,
color: '#465FFF',
fontFamily: 'Outfit',
formatter: () => {
const total = Object.values(stats.keywords.byStatus).reduce((a, b) => a + b, 0);
return total > 0 ? total.toString() : '0';
}
},
total: { show: false }
}
}
}
}
};
const series = Object.keys(stats.keywords.byStatus)
.filter(key => stats.keywords.byStatus[key] > 0)
.map(key => stats.keywords.byStatus[key]);
return { options, series };
}, [stats]);
const topClustersChart = useMemo(() => {
if (!stats || stats.clusters.topClusters.length === 0) return null;
const options: ApexOptions = {
chart: {
type: 'bar',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false },
height: 300
},
colors: ['#10B981'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 5,
dataLabels: {
position: 'top'
}
}
},
dataLabels: {
enabled: true,
formatter: (val: number) => val.toLocaleString(),
offsetX: 10
},
xaxis: {
categories: stats.clusters.topClusters.map(c => c.name),
labels: {
style: {
fontFamily: 'Outfit',
fontSize: '12px'
}
}
},
yaxis: {
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
tooltip: {
y: {
formatter: (val: number) => `${val.toLocaleString()} volume`
}
}
};
const series = [{
name: 'Search Volume',
data: stats.clusters.topClusters.map(c => c.volume)
}];
return { options, series };
}, [stats]);
const formatTimeAgo = (date: Date) => {
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
};
if (loading && !stats) {
return (
<>
<PageMeta title="Planner Dashboard - IGNY8" description="Content planning overview" />
<div className="flex items-center justify-center h-96">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-brand-500 border-t-transparent"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading dashboard data...</p>
</div>
</div>
</>
);
}
if (!stats && !loading) {
return (
<>
<PageMeta title="Planner Dashboard - IGNY8" description="Content planning overview" />
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">
{activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}
</p>
</div>
</>
);
}
if (!stats) return null;
return (
<>
<PageMeta title="Planner Dashboard - IGNY8" description="Content planning overview" />
<PageHeader
title="Planner Dashboard"
lastUpdated={lastUpdated}
showRefresh={true}
onRefresh={fetchDashboardData}
/>
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<EnhancedMetricCard
title="Total Keywords"
value={stats.keywords.total}
subtitle={`${stats.keywords.mapped} mapped • ${stats.keywords.unmapped} unmapped`}
icon={<ListIcon className="size-6" />}
accentColor="blue"
trend={0}
href="/planner/keywords"
/>
<EnhancedMetricCard
title="Clusters Built"
value={stats.clusters.total}
subtitle={`${stats.clusters.totalVolume.toLocaleString()} volume • ${stats.clusters.avgKeywords} avg keywords`}
icon={<GroupIcon className="size-6" />}
accentColor="green"
trend={0}
href="/planner/clusters"
/>
<EnhancedMetricCard
title="Ideas Generated"
value={stats.ideas.total}
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} pending`}
icon={<BoltIcon className="size-6" />}
accentColor="orange"
trend={0}
href="/planner/ideas"
/>
<EnhancedMetricCard
title="Mapping Progress"
value={`${keywordMappingPct}%`}
subtitle={`${stats.keywords.mapped} of ${stats.keywords.total} keywords mapped`}
icon={<PieChartIcon className="size-6" />}
accentColor="purple"
trend={0}
href="/planner/keywords"
/>
</div>
{/* Planner Modules */}
<ComponentCard title="Planner Modules" desc="Access all planning tools and features">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{plannerModules.map((module) => {
const Icon = module.icon;
return (
<Link
key={module.title}
to={module.path}
className="rounded-2xl border-2 border-slate-200 bg-white p-6 hover:shadow-xl hover:-translate-y-1 transition-all group"
>
<div className="flex items-start justify-between mb-4">
<div className={`inline-flex size-14 rounded-xl bg-gradient-to-br ${module.color} items-center justify-center text-white shadow-lg`}>
<Icon className="h-7 w-7" />
</div>
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2">{module.title}</h3>
<p className="text-sm text-slate-600 mb-4">{module.description}</p>
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-slate-900">{module.count}</div>
<div className="text-xs text-slate-500">{module.metric}</div>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] group-hover:translate-x-1 transition" />
</div>
</Link>
);
})}
</div>
</ComponentCard>
{/* Activity Chart & Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComponentCard title="Planning Activity" desc="Keywords, clusters, and ideas over the past week">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart options={chartOptions} series={chartSeries} type="area" height={300} />
</Suspense>
</ComponentCard>
<ComponentCard title="Recent Activity" desc="Latest planning actions and updates">
<div className="space-y-4">
{recentActivity.map((activity) => {
const Icon = activity.icon;
return (
<div
key={activity.id}
className="flex items-center gap-4 p-4 rounded-lg border border-slate-200 bg-white hover:shadow-md transition"
>
<div className={`size-10 rounded-lg bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center ${activity.color}`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-slate-900">{activity.type}</h4>
<span className="text-xs text-slate-500">{formatTimeAgo(activity.timestamp)}</span>
</div>
<p className="text-sm text-slate-600">{activity.description}</p>
</div>
</div>
);
})}
</div>
</ComponentCard>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{keywordsStatusChart && (
<ComponentCard title="Keywords by Status" desc="Distribution of keywords across statuses">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart
options={keywordsStatusChart.options}
series={keywordsStatusChart.series}
type="donut"
height={300}
/>
</Suspense>
</ComponentCard>
)}
{topClustersChart && (
<ComponentCard title="Top 5 Clusters by Volume" desc="Highest volume keyword clusters">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart
options={topClustersChart.options}
series={topClustersChart.series}
type="bar"
height={300}
/>
</Suspense>
</ComponentCard>
)}
</div>
{/* Progress Summary */}
<ComponentCard title="Planning Progress" desc="Track your planning workflow progress">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Keyword Mapping</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{keywordMappingPct}%</span>
</div>
<ProgressBar value={keywordMappingPct} color="primary" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.keywords.mapped} of {stats.keywords.total} keywords mapped
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Clusters With Ideas</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{clustersIdeasPct}%</span>
</div>
<ProgressBar value={clustersIdeasPct} color="success" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.clusters.withIdeas} of {stats.clusters.total} clusters have ideas
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Ideas Queued to Writer</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{ideasQueuedPct}%</span>
</div>
<ProgressBar value={ideasQueuedPct} color="warning" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.ideas.queued} of {stats.ideas.total} ideas queued
</p>
</div>
</div>
</ComponentCard>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Common planning tasks and shortcuts">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Link
to="/planner/keyword-opportunities"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<ListIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Add Keywords</h4>
<p className="text-sm text-slate-600">Discover opportunities</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
</Link>
<Link
to="/planner/clusters"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0bbf87] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
<GroupIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Auto Cluster</h4>
<p className="text-sm text-slate-600">Group keywords</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#0bbf87] transition" />
</Link>
<Link
to="/planner/ideas"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#ff7a00] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
<BoltIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Generate Ideas</h4>
<p className="text-sm text-slate-600">Create content ideas</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#ff7a00] transition" />
</Link>
<Link
to="/automation"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#5d4ae3] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
<PlugInIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Setup Automation</h4>
<p className="text-sm text-slate-600">Automate workflows</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#5d4ae3] transition" />
</Link>
</div>
</ComponentCard>
{/* Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ComponentCard title="How Planner Works" desc="Understanding the planning workflow">
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
<ListIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">Keyword Discovery</h4>
<p className="text-sm text-slate-600">
Discover high-volume keywords from our global database. Add keywords manually or import from keyword opportunities.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-md">
<GroupIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">AI Clustering</h4>
<p className="text-sm text-slate-600">
Automatically group related keywords into strategic clusters. Each cluster represents a content topic with shared search intent.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
<BoltIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">Idea Generation</h4>
<p className="text-sm text-slate-600">
Generate content ideas from clusters using AI. Each idea includes title, outline, and target keywords for content creation.
</p>
</div>
</div>
</div>
</ComponentCard>
<ComponentCard title="Getting Started" desc="Quick guide to using Planner">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-[#0693e3] text-white flex items-center justify-center font-bold text-sm">
1
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">Add Keywords</h4>
<p className="text-sm text-slate-600">
Start by adding keywords from the keyword opportunities page. You can search by volume, difficulty, or intent.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-[#0bbf87] text-white flex items-center justify-center font-bold text-sm">
2
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">Cluster Keywords</h4>
<p className="text-sm text-slate-600">
Use the auto-cluster feature to group related keywords. Review and refine clusters to match your content strategy.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-[#ff7a00] text-white flex items-center justify-center font-bold text-sm">
3
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">Generate Ideas</h4>
<p className="text-sm text-slate-600">
Create content ideas from your clusters. Queue ideas to the Writer module to start content creation.
</p>
</div>
</div>
</div>
</ComponentCard>
</div>
</div>
</>
);
}