Pre luanch plan phase 1 complete
This commit is contained in:
@@ -1,823 +0,0 @@
|
||||
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 {
|
||||
FileTextIcon,
|
||||
BoxIcon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
PencilIcon,
|
||||
BoltIcon,
|
||||
ArrowRightIcon,
|
||||
PaperPlaneIcon,
|
||||
PlugInIcon,
|
||||
} from "../../icons";
|
||||
import {
|
||||
fetchTasks,
|
||||
fetchContent,
|
||||
fetchContentImages,
|
||||
fetchTaxonomies,
|
||||
} from "../../services/api";
|
||||
import { useSiteStore } from "../../store/siteStore";
|
||||
import { useSectorStore } from "../../store/sectorStore";
|
||||
|
||||
interface WriterStats {
|
||||
tasks: {
|
||||
total: number;
|
||||
byStatus: Record<string, number>;
|
||||
pending: number;
|
||||
inProgress: number;
|
||||
completed: number;
|
||||
avgWordCount: number;
|
||||
totalWordCount: number;
|
||||
};
|
||||
content: {
|
||||
total: number;
|
||||
drafts: number;
|
||||
published: number;
|
||||
publishedToSite: number;
|
||||
scheduledForPublish: number;
|
||||
totalWordCount: number;
|
||||
avgWordCount: number;
|
||||
byContentType: Record<string, number>;
|
||||
};
|
||||
images: {
|
||||
total: number;
|
||||
generated: number;
|
||||
pending: number;
|
||||
failed: number;
|
||||
byType: Record<string, number>;
|
||||
};
|
||||
workflow: {
|
||||
tasksCreated: boolean;
|
||||
contentGenerated: boolean;
|
||||
imagesGenerated: boolean;
|
||||
readyToPublish: boolean;
|
||||
};
|
||||
productivity: {
|
||||
contentThisWeek: number;
|
||||
contentThisMonth: number;
|
||||
avgGenerationTime: number;
|
||||
publishRate: number;
|
||||
};
|
||||
taxonomies: number;
|
||||
attributes: number;
|
||||
}
|
||||
|
||||
export default function WriterDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector } = useSectorStore();
|
||||
|
||||
const [stats, setStats] = useState<WriterStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const [tasksRes, contentRes, imagesRes, taxonomiesRes] = await Promise.all([
|
||||
fetchTasks({ page_size: 1000, sector_id: activeSector?.id }),
|
||||
fetchContent({ page_size: 1000, sector_id: activeSector?.id }),
|
||||
fetchContentImages({ sector_id: activeSector?.id }),
|
||||
fetchTaxonomies({ page_size: 1000, sector_id: activeSector?.id }),
|
||||
]);
|
||||
|
||||
const tasks = tasksRes.results || [];
|
||||
const tasksByStatus: Record<string, number> = {};
|
||||
let pendingTasks = 0;
|
||||
let inProgressTasks = 0;
|
||||
let completedTasks = 0;
|
||||
let totalWordCount = 0;
|
||||
|
||||
tasks.forEach(t => {
|
||||
tasksByStatus[t.status || 'queued'] = (tasksByStatus[t.status || 'queued'] || 0) + 1;
|
||||
if (t.status === 'queued') pendingTasks++;
|
||||
else if (t.status === 'completed') completedTasks++;
|
||||
if (t.word_count) totalWordCount += t.word_count;
|
||||
});
|
||||
|
||||
const avgWordCount = tasks.length > 0 ? Math.round(totalWordCount / tasks.length) : 0;
|
||||
|
||||
const content = contentRes.results || [];
|
||||
let drafts = 0;
|
||||
let published = 0;
|
||||
let publishedToSite = 0;
|
||||
let scheduledForPublish = 0;
|
||||
let contentTotalWordCount = 0;
|
||||
const contentByType: Record<string, number> = {};
|
||||
|
||||
content.forEach(c => {
|
||||
if (c.status === 'draft') drafts++;
|
||||
else if (c.status === 'published') published++;
|
||||
// Count site_status for external publishing metrics
|
||||
if (c.site_status === 'published') publishedToSite++;
|
||||
else if (c.site_status === 'scheduled') scheduledForPublish++;
|
||||
if (c.word_count) contentTotalWordCount += c.word_count;
|
||||
});
|
||||
|
||||
const contentAvgWordCount = content.length > 0 ? Math.round(contentTotalWordCount / content.length) : 0;
|
||||
|
||||
const images = imagesRes.results || [];
|
||||
let generatedImages = 0;
|
||||
let pendingImages = 0;
|
||||
let failedImages = 0;
|
||||
const imagesByType: Record<string, number> = {};
|
||||
|
||||
images.forEach(imgGroup => {
|
||||
if (imgGroup.overall_status === 'complete') generatedImages++;
|
||||
else if (imgGroup.overall_status === 'pending' || imgGroup.overall_status === 'partial') pendingImages++;
|
||||
else if (imgGroup.overall_status === 'failed') failedImages++;
|
||||
|
||||
if (imgGroup.featured_image) {
|
||||
imagesByType['featured'] = (imagesByType['featured'] || 0) + 1;
|
||||
}
|
||||
if (imgGroup.in_article_images && imgGroup.in_article_images.length > 0) {
|
||||
imagesByType['in_article'] = (imagesByType['in_article'] || 0) + imgGroup.in_article_images.length;
|
||||
}
|
||||
});
|
||||
|
||||
const contentThisWeek = Math.floor(content.length * 0.3);
|
||||
const contentThisMonth = Math.floor(content.length * 0.7);
|
||||
const publishRate = content.length > 0 ? Math.round((published / content.length) * 100) : 0;
|
||||
|
||||
const taxonomies = taxonomiesRes.results || [];
|
||||
const taxonomyCount = taxonomies.length;
|
||||
// Note: Attributes are a subset of taxonomies with type 'product_attribute'
|
||||
const attributeCount = taxonomies.filter(t => t.taxonomy_type === 'product_attribute').length;
|
||||
|
||||
setStats({
|
||||
tasks: {
|
||||
total: tasks.length,
|
||||
byStatus: tasksByStatus,
|
||||
pending: pendingTasks,
|
||||
inProgress: inProgressTasks,
|
||||
completed: completedTasks,
|
||||
avgWordCount,
|
||||
totalWordCount
|
||||
},
|
||||
content: {
|
||||
total: content.length,
|
||||
drafts,
|
||||
published,
|
||||
publishedToSite,
|
||||
scheduledForPublish,
|
||||
totalWordCount: contentTotalWordCount,
|
||||
avgWordCount: contentAvgWordCount,
|
||||
byContentType: contentByType
|
||||
},
|
||||
images: {
|
||||
total: images.length,
|
||||
generated: generatedImages,
|
||||
pending: pendingImages,
|
||||
failed: failedImages,
|
||||
byType: imagesByType
|
||||
},
|
||||
workflow: {
|
||||
tasksCreated: tasks.length > 0,
|
||||
contentGenerated: content.length > 0,
|
||||
imagesGenerated: generatedImages > 0,
|
||||
readyToPublish: published > 0
|
||||
},
|
||||
productivity: {
|
||||
contentThisWeek,
|
||||
contentThisMonth,
|
||||
avgGenerationTime: 0,
|
||||
publishRate
|
||||
},
|
||||
taxonomies: taxonomyCount,
|
||||
attributes: attributeCount,
|
||||
});
|
||||
|
||||
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 completionRate = useMemo(() => {
|
||||
if (!stats || stats.tasks.total === 0) return 0;
|
||||
return Math.round((stats.tasks.completed / stats.tasks.total) * 100);
|
||||
}, [stats]);
|
||||
|
||||
const writerModules = [
|
||||
{
|
||||
title: "Tasks",
|
||||
description: "Content writing tasks and assignments",
|
||||
icon: FileTextIcon,
|
||||
color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
|
||||
path: "/writer/tasks",
|
||||
count: stats?.tasks.total || 0,
|
||||
metric: `${stats?.tasks.completed || 0} completed`,
|
||||
},
|
||||
{
|
||||
title: "Content",
|
||||
description: "Generated content and drafts",
|
||||
icon: PencilIcon,
|
||||
color: "from-[var(--color-success)] to-[var(--color-success-dark)]",
|
||||
path: "/writer/content",
|
||||
count: stats?.content.total || 0,
|
||||
metric: `${stats?.content.published || 0} published`,
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
description: "Generated images and assets",
|
||||
icon: BoxIcon,
|
||||
color: "from-[var(--color-warning)] to-[var(--color-warning-dark)]",
|
||||
path: "/writer/images",
|
||||
count: stats?.images.generated || 0,
|
||||
metric: `${stats?.images.pending || 0} pending`,
|
||||
},
|
||||
{
|
||||
title: "Published to Site",
|
||||
description: "Content published to external site",
|
||||
icon: PaperPlaneIcon,
|
||||
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
|
||||
path: "/writer/published",
|
||||
count: stats?.content.publishedToSite || 0,
|
||||
metric: stats?.content.scheduledForPublish ? `${stats.content.scheduledForPublish} scheduled` : "None scheduled",
|
||||
},
|
||||
{
|
||||
title: "Taxonomies",
|
||||
description: "Manage content taxonomies",
|
||||
icon: BoltIcon,
|
||||
color: "from-[var(--color-info)] to-[var(--color-info-dark)]",
|
||||
path: "/writer/taxonomies",
|
||||
count: stats?.taxonomies || 0,
|
||||
metric: `${stats?.taxonomies || 0} total`,
|
||||
},
|
||||
{
|
||||
title: "Attributes",
|
||||
description: "Manage content attributes",
|
||||
icon: PlugInIcon,
|
||||
color: "from-[var(--color-secondary)] to-[var(--color-secondary-dark)]",
|
||||
path: "/writer/attributes",
|
||||
count: stats?.attributes || 0,
|
||||
metric: `${stats?.attributes || 0} total`,
|
||||
},
|
||||
];
|
||||
|
||||
const recentActivity = [
|
||||
{
|
||||
id: 1,
|
||||
type: "Content Published",
|
||||
description: `${stats?.content.published || 0} pieces published to site`,
|
||||
timestamp: new Date(Date.now() - 30 * 60 * 1000),
|
||||
icon: PaperPlaneIcon,
|
||||
color: "text-success-600",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "Content Generated",
|
||||
description: `${stats?.content.total || 0} content pieces created`,
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
icon: PencilIcon,
|
||||
color: "text-brand-600",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "Images Generated",
|
||||
description: `${stats?.images.generated || 0} images created`,
|
||||
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
|
||||
icon: BoxIcon,
|
||||
color: "text-warning-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: "var(--color-gray-500)" } },
|
||||
},
|
||||
yaxis: {
|
||||
labels: { style: { colors: "var(--color-gray-500)" } },
|
||||
},
|
||||
legend: {
|
||||
position: "top",
|
||||
labels: { colors: "var(--color-gray-500)" },
|
||||
},
|
||||
colors: ["var(--color-primary)", "var(--color-success)", "var(--color-warning)"],
|
||||
grid: {
|
||||
borderColor: "var(--color-gray-200)",
|
||||
},
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
opacityFrom: 0.6,
|
||||
opacityTo: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const chartSeries = [
|
||||
{
|
||||
name: "Content Created",
|
||||
data: [12, 19, 15, 25, 22, 18, 24],
|
||||
},
|
||||
{
|
||||
name: "Tasks Completed",
|
||||
data: [8, 12, 10, 15, 14, 11, 16],
|
||||
},
|
||||
{
|
||||
name: "Images Generated",
|
||||
data: [5, 8, 6, 10, 9, 7, 11],
|
||||
},
|
||||
];
|
||||
|
||||
const tasksStatusChart = useMemo(() => {
|
||||
if (!stats) return null;
|
||||
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
type: 'donut',
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
toolbar: { show: false }
|
||||
},
|
||||
labels: Object.keys(stats.tasks.byStatus).filter(key => stats.tasks.byStatus[key] > 0),
|
||||
colors: ['var(--color-primary)', 'var(--color-warning)', 'var(--color-success)', 'var(--color-danger)', 'var(--color-purple)'],
|
||||
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: 'var(--color-primary)',
|
||||
fontFamily: 'Outfit',
|
||||
formatter: () => {
|
||||
const total = Object.values(stats.tasks.byStatus).reduce((a: number, b: number) => a + b, 0);
|
||||
return total > 0 ? total.toString() : '0';
|
||||
}
|
||||
},
|
||||
total: { show: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const series = Object.keys(stats.tasks.byStatus)
|
||||
.filter(key => stats.tasks.byStatus[key] > 0)
|
||||
.map(key => stats.tasks.byStatus[key]);
|
||||
|
||||
return { options, series };
|
||||
}, [stats]);
|
||||
|
||||
const contentStatusChart = useMemo(() => {
|
||||
if (!stats) return null;
|
||||
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
toolbar: { show: false },
|
||||
height: 300
|
||||
},
|
||||
colors: ['var(--color-primary)', 'var(--color-warning)', 'var(--color-success)'],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '55%',
|
||||
borderRadius: 5
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['Drafts', 'Published'],
|
||||
labels: {
|
||||
style: {
|
||||
fontFamily: 'Outfit'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontFamily: 'Outfit'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4
|
||||
}
|
||||
};
|
||||
|
||||
const series = [{
|
||||
name: 'Content',
|
||||
data: [stats.content.drafts, stats.content.published]
|
||||
}];
|
||||
|
||||
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="Writer Dashboard - IGNY8" description="Content creation 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="Writer Dashboard - IGNY8" description="Content creation 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="Content Creation Dashboard - IGNY8" description="Track your writing progress and productivity" />
|
||||
<PageHeader
|
||||
title="Content Creation 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 Tasks"
|
||||
value={stats.tasks.total}
|
||||
subtitle={`${stats.tasks.completed} completed • ${stats.tasks.pending} pending`}
|
||||
icon={<FileTextIcon className="size-6" />}
|
||||
accentColor="blue"
|
||||
trend={0}
|
||||
href="/writer/tasks"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Content Pieces"
|
||||
value={stats.content.total}
|
||||
subtitle={`${stats.content.published} published • ${stats.content.drafts} drafts`}
|
||||
icon={<PencilIcon className="size-6" />}
|
||||
accentColor="green"
|
||||
trend={0}
|
||||
href="/writer/content"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Images Generated"
|
||||
value={stats.images.generated}
|
||||
subtitle={`${stats.images.total} total • ${stats.images.pending} pending`}
|
||||
icon={<BoxIcon className="size-6" />}
|
||||
accentColor="orange"
|
||||
trend={0}
|
||||
href="/writer/images"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Publish Rate"
|
||||
value={`${stats.productivity.publishRate}%`}
|
||||
subtitle={`${stats.content.published} of ${stats.content.total} published`}
|
||||
icon={<PaperPlaneIcon className="size-6" />}
|
||||
accentColor="purple"
|
||||
trend={0}
|
||||
href="/writer/published"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Writer Modules */}
|
||||
<ComponentCard title="Writer Modules" desc="Access all content creation tools and features">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{writerModules.map((module) => {
|
||||
const Icon = module.icon;
|
||||
return (
|
||||
<Link
|
||||
key={module.title}
|
||||
to={module.path}
|
||||
className="rounded-2xl border-2 border-gray-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-gray-900 mb-2">{module.title}</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">{module.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">{module.count}</div>
|
||||
<div className="text-xs text-gray-500">{module.metric}</div>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-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="Content Creation Activity" desc="Tasks, content, and images 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 content creation 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-gray-200 bg-white hover:shadow-md transition"
|
||||
>
|
||||
<div className={`size-10 rounded-lg bg-gradient-to-br from-gray-100 to-gray-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-gray-900">{activity.type}</h4>
|
||||
<span className="text-xs text-gray-500">{formatTimeAgo(activity.timestamp)}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{activity.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{tasksStatusChart && (
|
||||
<ComponentCard title="Tasks by Status" desc="Task distribution across statuses">
|
||||
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
|
||||
<Chart
|
||||
options={tasksStatusChart.options}
|
||||
series={tasksStatusChart.series}
|
||||
type="donut"
|
||||
height={300}
|
||||
/>
|
||||
</Suspense>
|
||||
</ComponentCard>
|
||||
)}
|
||||
|
||||
{contentStatusChart && (
|
||||
<ComponentCard title="Content by Status" desc="Distribution across workflow stages">
|
||||
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
|
||||
<Chart
|
||||
options={contentStatusChart.options}
|
||||
series={contentStatusChart.series}
|
||||
type="bar"
|
||||
height={300}
|
||||
/>
|
||||
</Suspense>
|
||||
</ComponentCard>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Productivity Metrics */}
|
||||
<ComponentCard title="Productivity Metrics" desc="Content creation performance tracking">
|
||||
<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">Task Completion</span>
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{completionRate}%</span>
|
||||
</div>
|
||||
<ProgressBar value={completionRate} color="primary" size="md" />
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{stats.tasks.completed} of {stats.tasks.total} tasks completed
|
||||
</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">Publish Rate</span>
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{stats.productivity.publishRate}%</span>
|
||||
</div>
|
||||
<ProgressBar value={stats.productivity.publishRate} color="success" size="md" />
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{stats.content.published} of {stats.content.total} content published
|
||||
</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">Image Generation</span>
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">
|
||||
{stats.images.total > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={stats.images.total > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}
|
||||
color="warning"
|
||||
size="md"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{stats.images.generated} of {stats.images.total} images generated
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<ComponentCard title="Quick Actions" desc="Common content creation tasks and shortcuts">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Link
|
||||
to="/writer/tasks"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-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">
|
||||
<FileTextIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Create Task</h4>
|
||||
<p className="text-sm text-gray-600">New writing task</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/writer/content"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-success-500 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">
|
||||
<PencilIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Generate Content</h4>
|
||||
<p className="text-sm text-gray-600">AI content creation</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-success-500 transition" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/writer/images"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-warning-500 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">
|
||||
<BoxIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Generate Images</h4>
|
||||
<p className="text-sm text-gray-600">Create visuals</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/writer/published"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-purple-500 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">
|
||||
<PaperPlaneIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Publish Content</h4>
|
||||
<p className="text-sm text-gray-600">Publish to Site</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-purple-500 transition" />
|
||||
</Link>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Info Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<ComponentCard title="How Writer Works" desc="Understanding the content creation 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">
|
||||
<FileTextIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Task Creation</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
Create writing tasks from content ideas. Each task includes target keywords, outline, and word count requirements.
|
||||
</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">
|
||||
<PencilIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-1">AI Content Generation</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
Generate full content pieces using AI. Content is created based on your prompts, author profiles, and brand guidelines.
|
||||
</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">
|
||||
<BoxIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Image Generation</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
Automatically generate featured images and in-article images for your content. Images are optimized for SEO and engagement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Getting Started" desc="Quick guide to using Writer">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 size-8 rounded-full bg-brand-500 text-white flex items-center justify-center font-bold text-sm">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Create Tasks</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
Start by creating writing tasks from content ideas in the Planner module. Tasks define what content needs to be written.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 size-8 rounded-full bg-success-500 text-white flex items-center justify-center font-bold text-sm">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Generate Content</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
Use AI to generate content from tasks. Review and edit generated content before publishing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 size-8 rounded-full bg-warning-500 text-white flex items-center justify-center font-bold text-sm">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Publish</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
Once content is reviewed and images are generated, publish directly to WordPress or export for manual publishing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Drafts Page - Filtered Tasks with status='draft'
|
||||
* Consistent with Keywords page layout, structure and design
|
||||
*/
|
||||
|
||||
import Tasks from './Tasks';
|
||||
|
||||
export default function Drafts() {
|
||||
// Drafts is just Tasks with status='draft' filter applied
|
||||
// For now, we'll use the Tasks component but could enhance it later
|
||||
// to show only draft status tasks by default
|
||||
return <Tasks />;
|
||||
}
|
||||
Reference in New Issue
Block a user