This commit is contained in:
Desktop
2025-11-12 20:44:59 +05:00
parent b07d0f518a
commit 9692a5ed2e
3 changed files with 223 additions and 206 deletions

View File

@@ -1,10 +1,11 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import PageMeta from "../../components/common/PageMeta"; import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard"; import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress"; import { ProgressBar } from "../../components/ui/progress";
import Chart from "react-apexcharts";
import { ApexOptions } from "apexcharts"; import { ApexOptions } from "apexcharts";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import { import {
ListIcon, ListIcon,
GroupIcon, GroupIcon,
@@ -445,18 +446,18 @@ export default function PlannerDashboard() {
{/* Top Status Cards */} {/* Top Status Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<Link <Link
to="/planner/keywords" to="/planner/keywords"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden" className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
> >
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500"></div> <div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500"></div>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Keywords Ready</p> <p className="text-sm text-gray-500 dark:text-gray-400">Keywords Ready</p>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90"> <h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.keywords.total.toLocaleString()} {stats.keywords.total.toLocaleString()}
</h4> </h4>
{trends.keywords !== 0 && ( {trends.keywords !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.keywords > 0 ? 'text-success-500' : 'text-error-500'}`}> <div className={`flex items-center gap-1 text-xs ${trends.keywords > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.keywords > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />} {trends.keywords > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
@@ -464,28 +465,28 @@ export default function PlannerDashboard() {
</div> </div>
)} )}
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.keywords.mapped} mapped {stats.keywords.unmapped} unmapped {stats.keywords.mapped} mapped {stats.keywords.unmapped} unmapped
</p> </p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-xl dark:bg-blue-500/10 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<ListIcon className="text-brand-500 size-6" />
</div>
</div> </div>
</Link> <div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-xl dark:bg-blue-500/10 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<ListIcon className="text-brand-500 size-6" />
</div>
</div>
</Link>
<Link <Link
to="/planner/clusters" to="/planner/clusters"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden" className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
> >
<div className="absolute left-0 top-0 bottom-0 w-1 bg-success-500"></div> <div className="absolute left-0 top-0 bottom-0 w-1 bg-success-500"></div>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Clusters Built</p> <p className="text-sm text-gray-500 dark:text-gray-400">Clusters Built</p>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90"> <h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.clusters.total.toLocaleString()} {stats.clusters.total.toLocaleString()}
</h4> </h4>
{trends.clusters !== 0 && ( {trends.clusters !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.clusters > 0 ? 'text-success-500' : 'text-error-500'}`}> <div className={`flex items-center gap-1 text-xs ${trends.clusters > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.clusters > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />} {trends.clusters > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
@@ -493,28 +494,28 @@ export default function PlannerDashboard() {
</div> </div>
)} )}
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.clusters.totalVolume.toLocaleString()} total volume {stats.clusters.avgKeywords} avg keywords {stats.clusters.totalVolume.toLocaleString()} total volume {stats.clusters.avgKeywords} avg keywords
</p> </p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-green-50 rounded-xl dark:bg-green-500/10 group-hover:bg-green-100 dark:group-hover:bg-green-500/20 transition-colors">
<GroupIcon className="text-success-500 size-6" />
</div>
</div> </div>
</Link> <div className="flex items-center justify-center w-12 h-12 bg-green-50 rounded-xl dark:bg-green-500/10 group-hover:bg-green-100 dark:group-hover:bg-green-500/20 transition-colors">
<GroupIcon className="text-success-500 size-6" />
</div>
</div>
</Link>
<Link <Link
to="/planner/ideas" to="/planner/ideas"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden" className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
> >
<div className="absolute left-0 top-0 bottom-0 w-1 bg-warning-500"></div> <div className="absolute left-0 top-0 bottom-0 w-1 bg-warning-500"></div>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Ideas Generated</p> <p className="text-sm text-gray-500 dark:text-gray-400">Ideas Generated</p>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90"> <h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.ideas.total.toLocaleString()} {stats.ideas.total.toLocaleString()}
</h4> </h4>
{trends.ideas !== 0 && ( {trends.ideas !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.ideas > 0 ? 'text-success-500' : 'text-error-500'}`}> <div className={`flex items-center gap-1 text-xs ${trends.ideas > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.ideas > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />} {trends.ideas > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
@@ -522,48 +523,48 @@ export default function PlannerDashboard() {
</div> </div>
)} )}
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.ideas.queued} queued {stats.ideas.notQueued} pending {stats.ideas.queued} queued {stats.ideas.notQueued} pending
</p> </p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-amber-50 rounded-xl dark:bg-amber-500/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-500/20 transition-colors">
<BoltIcon className="text-warning-500 size-6" />
</div>
</div> </div>
</Link> <div className="flex items-center justify-center w-12 h-12 bg-amber-50 rounded-xl dark:bg-amber-500/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-500/20 transition-colors">
<BoltIcon className="text-warning-500 size-6" />
</div>
</div>
</Link>
<Link <Link
to="/planner/keywords" to="/planner/keywords"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden" className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
> >
<div className="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div> <div className="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Mapping Progress</p> <p className="text-sm text-gray-500 dark:text-gray-400">Mapping Progress</p>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90"> <h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
{keywordMappingPct}% {keywordMappingPct}%
</h4> </h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.keywords.mapped} of {stats.keywords.total} keywords mapped {stats.keywords.mapped} of {stats.keywords.total} keywords mapped
</p> </p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-xl dark:bg-purple-500/10 group-hover:bg-purple-100 dark:group-hover:bg-purple-500/20 transition-colors">
<PieChartIcon className="text-purple-500 size-6" />
</div>
</div> </div>
</Link> <div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-xl dark:bg-purple-500/10 group-hover:bg-purple-100 dark:group-hover:bg-purple-500/20 transition-colors">
</div> <PieChartIcon className="text-purple-500 size-6" />
</div>
</div>
</Link>
</div>
{/* Planner Workflow Steps */} {/* Planner Workflow Steps */}
<ComponentCard title="Planner Workflow Steps" desc="Track your planning progress"> <ComponentCard title="Planner Workflow Steps" desc="Track your planning progress">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{workflowSteps.map((step) => ( {workflowSteps.map((step) => (
<Link <Link
key={step.number} key={step.number}
to={step.path} to={step.path}
className="rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-800 hover:border-brand-300 hover:bg-gradient-to-br hover:from-brand-50 hover:to-white dark:hover:from-brand-500/10 dark:hover:to-gray-800/50 transition-all group" className="rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-800 hover:border-brand-300 hover:bg-gradient-to-br hover:from-brand-50 hover:to-white dark:hover:from-brand-500/10 dark:hover:to-gray-800/50 transition-all group"
> >
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold ${ <div className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold ${
step.status === "completed" step.status === "completed"
? "bg-success-500 text-white" ? "bg-success-500 text-white"
@@ -572,150 +573,156 @@ export default function PlannerDashboard() {
: "bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400" : "bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
}`}> }`}>
{step.status === "completed" ? <CheckCircleIcon className="size-5" /> : step.number} {step.status === "completed" ? <CheckCircleIcon className="size-5" /> : step.number}
</div>
<h4 className="font-medium text-gray-800 dark:text-white/90">{step.title}</h4>
</div> </div>
<h4 className="font-medium text-gray-800 dark:text-white/90">{step.title}</h4>
</div>
<div className="flex items-center justify-between text-sm mb-2"> <div className="flex items-center justify-between text-sm mb-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{step.status === "completed" ? ( {step.status === "completed" ? (
<> <>
<CheckCircleIcon className="size-4 text-success-500" /> <CheckCircleIcon className="size-4 text-success-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Completed</span> <span className="text-gray-600 dark:text-gray-300 font-medium">Completed</span>
</> </>
) : ( ) : (
<> <>
<TimeIcon className="size-4 text-amber-500" /> <TimeIcon className="size-4 text-amber-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Pending</span> <span className="text-gray-600 dark:text-gray-300 font-medium">Pending</span>
</> </>
)} )}
</div>
</div> </div>
{step.count !== null && ( </div>
{step.count !== null && (
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2"> <p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
{step.count} {step.title.includes("Keywords") ? "keywords" : step.title.includes("Clusters") ? "clusters" : step.title.includes("Ideas") ? "ideas" : "items"} {step.count} {step.title.includes("Keywords") ? "keywords" : step.title.includes("Clusters") ? "clusters" : step.title.includes("Ideas") ? "ideas" : "items"}
</p> </p>
)} )}
{step.status === "pending" && ( {step.status === "pending" && (
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
navigate(step.path); navigate(step.path);
}} }}
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer group-hover:translate-x-1 transition-transform" className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer group-hover:translate-x-1 transition-transform"
> >
Start Now <ArrowRightIcon className="size-3" /> Start Now <ArrowRightIcon className="size-3" />
</button> </button>
)} )}
</Link> </Link>
))} ))}
</div>
</ComponentCard>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Progress Summary */}
<ComponentCard title="Progress & Readiness Summary" desc="Planning workflow progress tracking" className="lg:col-span-1">
<div className="space-y-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> </div>
</ComponentCard> </ComponentCard>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Progress Summary */}
<ComponentCard title="Progress & Readiness Summary" desc="Planning workflow progress tracking" className="lg:col-span-1">
<div className="space-y-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>
{/* Top 5 Clusters */} {/* Top 5 Clusters */}
<ComponentCard title="Top 5 Clusters by Volume" desc="Highest volume keyword clusters" className="lg:col-span-2"> <ComponentCard title="Top 5 Clusters by Volume" desc="Highest volume keyword clusters" className="lg:col-span-2">
{topClustersChart ? ( {topClustersChart ? (
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={topClustersChart.options} <Chart
series={topClustersChart.series} options={topClustersChart.options}
type="bar" series={topClustersChart.series}
height={300} type="bar"
/> height={300}
/>
</Suspense>
) : ( ) : (
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> <div className="text-center py-8 text-gray-500 dark:text-gray-400">
No clusters data available No clusters data available
</div> </div>
)} )}
</ComponentCard> </ComponentCard>
</div> </div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Keywords by Status */} {/* Keywords by Status */}
{keywordsStatusChart && ( {keywordsStatusChart && (
<ComponentCard title="Keywords by Status" desc="Distribution of keywords across statuses"> <ComponentCard title="Keywords by Status" desc="Distribution of keywords across statuses">
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={keywordsStatusChart.options} <Chart
series={keywordsStatusChart.series} options={keywordsStatusChart.options}
type="donut" series={keywordsStatusChart.series}
height={300} type="donut"
/> height={300}
/>
</Suspense>
</ComponentCard> </ComponentCard>
)} )}
{/* Ideas by Status */} {/* Ideas by Status */}
{ideasStatusChart && ( {ideasStatusChart && (
<ComponentCard title="Ideas by Status" desc="Content ideas workflow status"> <ComponentCard title="Ideas by Status" desc="Content ideas workflow status">
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={ideasStatusChart.options} <Chart
series={ideasStatusChart.series} options={ideasStatusChart.options}
type="bar" series={ideasStatusChart.series}
height={300} type="bar"
/> height={300}
/>
</Suspense>
</ComponentCard> </ComponentCard>
)} )}
</div> </div>
{/* Next Actions */} {/* Next Actions */}
{nextActions.length > 0 && ( {nextActions.length > 0 && (
<ComponentCard title="Next Actions" desc="Actionable items requiring attention"> <ComponentCard title="Next Actions" desc="Actionable items requiring attention">
<div className="space-y-3"> <div className="space-y-3">
{nextActions.map((action, index) => ( {nextActions.map((action, index) => (
<div <div
key={index} key={index}
className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-gray-50 to-white dark:from-gray-900/50 dark:to-gray-800/50 border border-gray-200 dark:border-gray-800 hover:border-brand-300 dark:hover:border-brand-500/30 transition-all group" className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-gray-50 to-white dark:from-gray-900/50 dark:to-gray-800/50 border border-gray-200 dark:border-gray-800 hover:border-brand-300 dark:hover:border-brand-500/30 transition-all group"
> >
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{action.text}</span> <span className="text-sm font-medium text-gray-700 dark:text-gray-300">{action.text}</span>
<Link <Link
to={action.path} to={action.path}
className="inline-flex items-center gap-2 text-sm font-medium text-brand-500 hover:text-brand-600 group-hover:translate-x-1 transition-transform" className="inline-flex items-center gap-2 text-sm font-medium text-brand-500 hover:text-brand-600 group-hover:translate-x-1 transition-transform"
> >
{action.action} {action.action}
<ArrowRightIcon className="size-4" /> <ArrowRightIcon className="size-4" />
</Link> </Link>
</div>
))}
</div> </div>
</ComponentCard> ))}
</div>
</ComponentCard>
)} )}
</div> </div>
</> </>

View File

@@ -1,10 +1,11 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import PageMeta from "../../components/common/PageMeta"; import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard"; import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress"; import { ProgressBar } from "../../components/ui/progress";
import Chart from "react-apexcharts";
import { ApexOptions } from "apexcharts"; import { ApexOptions } from "apexcharts";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import { import {
FileTextIcon, FileTextIcon,
BoxIcon, BoxIcon,
@@ -748,12 +749,14 @@ export default function WriterDashboard() {
{/* Content Status Chart */} {/* Content Status Chart */}
{contentStatusChart && ( {contentStatusChart && (
<ComponentCard title="Content by Status" desc="Distribution across workflow stages" className="lg:col-span-2"> <ComponentCard title="Content by Status" desc="Distribution across workflow stages" className="lg:col-span-2">
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={contentStatusChart.options} <Chart
series={contentStatusChart.series} options={contentStatusChart.options}
type="bar" series={contentStatusChart.series}
height={300} type="bar"
/> height={300}
/>
</Suspense>
</ComponentCard> </ComponentCard>
)} )}
</div> </div>
@@ -762,24 +765,28 @@ export default function WriterDashboard() {
{/* Tasks by Status */} {/* Tasks by Status */}
{tasksStatusChart && ( {tasksStatusChart && (
<ComponentCard title="Tasks by Status" desc="Task distribution across statuses"> <ComponentCard title="Tasks by Status" desc="Task distribution across statuses">
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={tasksStatusChart.options} <Chart
series={tasksStatusChart.series} options={tasksStatusChart.options}
type="donut" series={tasksStatusChart.series}
height={300} type="donut"
/> height={300}
/>
</Suspense>
</ComponentCard> </ComponentCard>
)} )}
{/* Images by Type */} {/* Images by Type */}
{imagesTypeChart ? ( {imagesTypeChart ? (
<ComponentCard title="Images by Type" desc="Image generation breakdown"> <ComponentCard title="Images by Type" desc="Image generation breakdown">
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={imagesTypeChart.options} <Chart
series={imagesTypeChart.series} options={imagesTypeChart.options}
type="bar" series={imagesTypeChart.series}
height={300} type="bar"
/> height={300}
/>
</Suspense>
</ComponentCard> </ComponentCard>
) : ( ) : (
<ComponentCard title="Images Overview" desc="Image generation status"> <ComponentCard title="Images Overview" desc="Image generation status">
@@ -804,12 +811,14 @@ export default function WriterDashboard() {
{/* Productivity Chart */} {/* Productivity Chart */}
{productivityChart && ( {productivityChart && (
<ComponentCard title="Content Creation Trend" desc="Content created this week and month"> <ComponentCard title="Content Creation Trend" desc="Content created this week and month">
<Chart <Suspense fallback={<div className="flex items-center justify-center h-[200px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
options={productivityChart.options} <Chart
series={productivityChart.series} options={productivityChart.options}
type="area" series={productivityChart.series}
height={200} type="area"
/> height={200}
/>
</Suspense>
</ComponentCard> </ComponentCard>
)} )}

View File

@@ -25,6 +25,9 @@ export default defineConfig(({ mode }) => {
'clsx', 'clsx',
'tailwind-merge', 'tailwind-merge',
'zustand', 'zustand',
// Include apexcharts for proper module resolution
'apexcharts',
'react-apexcharts',
], ],
// Exclude heavy dependencies that are only used in specific pages // Exclude heavy dependencies that are only used in specific pages
// They will be lazy-loaded when needed // They will be lazy-loaded when needed
@@ -35,8 +38,6 @@ export default defineConfig(({ mode }) => {
'@fullcalendar/list', '@fullcalendar/list',
'@fullcalendar/react', '@fullcalendar/react',
'@fullcalendar/timegrid', '@fullcalendar/timegrid',
'apexcharts',
'react-apexcharts',
'@react-jvectormap/core', '@react-jvectormap/core',
'@react-jvectormap/world', '@react-jvectormap/world',
'react-dnd', 'react-dnd',