Files
igny8/frontend/src/pages/Writer/Dashboard.tsx
2025-11-12 23:29:31 +05:00

856 lines
30 KiB
TypeScript

import { useEffect, useState, useMemo, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import WorkflowPipeline, { WorkflowStep } from "../../components/dashboard/WorkflowPipeline";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
FileTextIcon,
BoxIcon,
CheckCircleIcon,
ClockIcon,
PencilIcon,
BoltIcon,
ArrowUpIcon,
ArrowDownIcon,
ArrowRightIcon
} from "../../icons";
import {
fetchTasks,
fetchContent,
fetchImages
} from "../../services/api";
import { useSiteStore } from "../../store/siteStore";
import { useSectorStore } from "../../store/sectorStore";
import PageHeader from "../../components/common/PageHeader";
interface WriterStats {
tasks: {
total: number;
byStatus: Record<string, number>;
pending: number;
inProgress: number;
completed: number;
avgWordCount: number;
totalWordCount: number;
};
content: {
total: number;
drafts: number;
review: number;
published: 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;
};
}
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 [trends, setTrends] = useState<{
tasks: number;
content: number;
images: number;
}>({ tasks: 0, content: 0, images: 0 });
// Fetch real data
const fetchDashboardData = async () => {
try {
setLoading(true);
// Fetch all data in parallel
const [tasksRes, contentRes, imagesRes] = await Promise.all([
fetchTasks({ page_size: 1000, sector_id: activeSector?.id }),
fetchContent({ page_size: 1000, sector_id: activeSector?.id }),
fetchImages({ page_size: 1000, sector_id: activeSector?.id })
]);
// Process tasks
const tasks = tasksRes.results || [];
const tasksByStatus: Record<string, number> = {};
let totalWordCount = 0;
let wordCountCount = 0;
tasks.forEach(t => {
tasksByStatus[t.status || 'draft'] = (tasksByStatus[t.status || 'draft'] || 0) + 1;
if (t.word_count) {
totalWordCount += t.word_count;
wordCountCount++;
}
});
const pendingTasks = tasks.filter(t => t.status === 'draft' || t.status === 'pending').length;
const inProgressTasks = tasks.filter(t => t.status === 'in_progress' || t.status === 'review').length;
const completedTasks = tasks.filter(t => t.status === 'completed' || t.status === 'published').length;
const avgWordCount = wordCountCount > 0 ? Math.round(totalWordCount / wordCountCount) : 0;
// Process content
const content = contentRes.results || [];
const contentByStatus: Record<string, number> = {};
const contentByType: Record<string, number> = {};
let contentTotalWords = 0;
let contentWordCount = 0;
content.forEach(c => {
contentByStatus[c.status || 'draft'] = (contentByStatus[c.status || 'draft'] || 0) + 1;
if (c.word_count) {
contentTotalWords += c.word_count;
contentWordCount++;
}
});
const drafts = content.filter(c => c.status === 'draft').length;
const review = content.filter(c => c.status === 'review').length;
const published = content.filter(c => c.status === 'published').length;
const contentAvgWordCount = contentWordCount > 0 ? Math.round(contentTotalWords / contentWordCount) : 0;
// Process images
const images = imagesRes.results || [];
const imagesByStatus: Record<string, number> = {};
const imagesByType: Record<string, number> = {};
images.forEach(img => {
imagesByStatus[img.status || 'pending'] = (imagesByStatus[img.status || 'pending'] || 0) + 1;
if (img.image_type) {
imagesByType[img.image_type] = (imagesByType[img.image_type] || 0) + 1;
}
});
const generatedImages = images.filter(img => img.status === 'generated' && img.image_url).length;
const pendingImages = images.filter(img => (img.status === 'pending' || img.status === 'draft') || !img.image_url).length;
const failedImages = images.filter(img => img.status === 'failed' || img.status === 'error').length;
// Calculate productivity metrics
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const contentThisWeek = content.filter(c => {
if (!c.generated_at) return false;
const created = new Date(c.generated_at);
return created >= weekAgo;
}).length;
const contentThisMonth = content.filter(c => {
if (!c.generated_at) return false;
const created = new Date(c.generated_at);
return created >= monthAgo;
}).length;
const publishRate = content.length > 0 ? Math.round((published / content.length) * 100) : 0;
// Calculate trends
if (stats) {
setTrends({
tasks: tasks.length - stats.tasks.total,
content: content.length - stats.content.total,
images: images.length - stats.images.total
});
}
setStats({
tasks: {
total: tasks.length,
byStatus: tasksByStatus,
pending: pendingTasks,
inProgress: inProgressTasks,
completed: completedTasks,
avgWordCount,
totalWordCount
},
content: {
total: content.length,
drafts,
review,
published,
totalWordCount: contentTotalWords,
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, // Would need task timestamps to calculate
publishRate
}
});
setLastUpdated(new Date());
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
// Initial load and periodic refresh
useEffect(() => {
// Allow loading for all sectors (when activeSector is null) or specific sector
fetchDashboardData();
// Refresh every 30 seconds
const interval = setInterval(fetchDashboardData, 30000);
return () => clearInterval(interval);
}, [activeSector?.id, activeSite?.id]);
// Chart data for tasks by status
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: ['#465FFF', '#F59E0B', '#10B981', '#EF4444', '#8B5CF6'],
legend: {
position: 'bottom',
fontFamily: 'Outfit',
show: true
},
dataLabels: {
enabled: false // Disable labels on pie slices
},
tooltip: {
enabled: true,
y: {
formatter: (val: number, { seriesIndex, w }: any) => {
const label = w.globals.labels[seriesIndex] || '';
return `${label}: ${val}`;
}
}
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: {
show: false // Hide "Total" label
},
value: {
show: true,
fontSize: '24px',
fontWeight: 700,
color: '#465FFF',
fontFamily: 'Outfit',
formatter: () => {
const total = Object.values(stats.tasks.byStatus).reduce((a, b) => a + b, 0);
return total > 0 ? total.toString() : '0';
}
},
total: {
show: false // Hide total label
}
}
}
}
}
};
const series = Object.keys(stats.tasks.byStatus)
.filter(key => stats.tasks.byStatus[key] > 0)
.map(key => stats.tasks.byStatus[key]);
return { options, series };
}, [stats]);
// Chart data for content by status
const contentStatusChart = useMemo(() => {
if (!stats) return null;
const options: ApexOptions = {
chart: {
type: 'bar',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false },
height: 250
},
colors: ['#465FFF', '#F59E0B', '#10B981'],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
borderRadius: 5
}
},
dataLabels: {
enabled: true
},
xaxis: {
categories: ['Drafts', 'In Review', 'Published'],
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
yaxis: {
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
grid: {
strokeDashArray: 4
}
};
const series = [{
name: 'Content',
data: [stats.content.drafts, stats.content.review, stats.content.published]
}];
return { options, series };
}, [stats]);
// Chart data for images by type
const imagesTypeChart = useMemo(() => {
if (!stats || Object.keys(stats.images.byType).length === 0) return null;
const options: ApexOptions = {
chart: {
type: 'bar',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false },
height: 250
},
colors: ['#10B981'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 5
}
},
dataLabels: {
enabled: true
},
xaxis: {
categories: Object.keys(stats.images.byType),
labels: {
style: {
fontFamily: 'Outfit',
fontSize: '12px'
}
}
},
yaxis: {
labels: {
style: {
fontFamily: 'Outfit'
}
}
}
};
const series = [{
name: 'Images',
data: Object.values(stats.images.byType)
}];
return { options, series };
}, [stats]);
// Productivity chart (content over time - simplified)
const productivityChart = useMemo(() => {
if (!stats) return null;
const options: ApexOptions = {
chart: {
type: 'area',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false },
height: 200
},
colors: ['#465FFF'],
stroke: {
curve: 'smooth',
width: 2
},
fill: {
type: 'gradient',
gradient: {
opacityFrom: 0.6,
opacityTo: 0.1
}
},
xaxis: {
categories: ['Week', 'Month'],
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
yaxis: {
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
grid: {
strokeDashArray: 4
},
dataLabels: {
enabled: true
}
};
const series = [{
name: 'Content Created',
data: [stats.productivity.contentThisWeek, stats.productivity.contentThisMonth]
}];
return { options, series };
}, [stats]);
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; // Still loading
}
const workflowSteps = [
{
number: 1,
title: "Create Tasks",
status: stats.workflow.tasksCreated ? "completed" : "pending",
count: stats.tasks.total,
path: "/writer/tasks"
},
{
number: 2,
title: "Generate Content",
status: stats.workflow.contentGenerated ? "completed" : "pending",
count: stats.content.total,
path: "/writer/content"
},
{
number: 3,
title: "Generate Images",
status: stats.workflow.imagesGenerated ? "completed" : "pending",
count: stats.images.generated,
path: "/writer/images"
},
{
number: 4,
title: "Publish",
status: stats.workflow.readyToPublish ? "completed" : "pending",
count: stats.content.published,
path: "/writer/published"
},
];
const completionRate = stats.tasks.total > 0
? Math.round((stats.tasks.completed / stats.tasks.total) * 100)
: 0;
const nextActions = [
...(stats.tasks.pending > 0 ? [{
text: `${stats.tasks.pending} tasks pending content generation`,
action: "Generate Content",
path: "/writer/tasks"
}] : []),
...(stats.content.drafts > 0 ? [{
text: `${stats.content.drafts} drafts ready for review`,
action: "Review Content",
path: "/writer/content"
}] : []),
...(stats.images.pending > 0 ? [{
text: `${stats.images.pending} images pending generation`,
action: "Generate Images",
path: "/writer/images"
}] : []),
...(stats.content.review > 0 ? [{
text: `${stats.content.review} content pieces ready to publish`,
action: "Publish Content",
path: "/writer/published"
}] : [])
];
return (
<>
<PageMeta title="Writer Dashboard - IGNY8" description="Content creation overview" />
<div className="space-y-5 sm:space-y-6">
{/* Header with site/sector info and controls */}
<PageHeader
title="Writer Dashboard"
lastUpdated={lastUpdated}
showRefresh={true}
onRefresh={fetchDashboardData}
badge={{ icon: <PencilIcon />, color: 'blue' }}
/>
{/* Hero Section - Key Metric */}
<div className="rounded-2xl border border-gray-200 bg-gradient-to-br from-brand-50 to-white dark:from-brand-500/10 dark:to-gray-800/50 dark:border-gray-800 p-6 md:p-8">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Content Creation Progress</p>
<h3 className="mt-2 text-3xl font-bold text-gray-800 dark:text-white/90">
{stats.content.published > 0 ? (
<>
{stats.content.published} Content Pieces Published
</>
) : stats.content.review > 0 ? (
<>
{stats.content.review} Pieces Ready to Publish
</>
) : stats.content.drafts > 0 ? (
<>
{stats.content.drafts} Drafts Ready for Review
</>
) : stats.tasks.total > 0 ? (
<>
{stats.tasks.total} Tasks Created
</>
) : (
<>
Ready to Create Content
</>
)}
</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{stats.tasks.total} tasks {stats.content.total} content pieces {stats.images.generated} images generated
</p>
</div>
<div className="hidden md:flex items-center gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-brand-500">{completionRate}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Complete</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-success-500">{stats.productivity.publishRate}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Published</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-warning-500">
{stats.images.generated > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Images</div>
</div>
</div>
</div>
</div>
{/* Enhanced Metric Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<EnhancedMetricCard
title="Total Tasks"
value={stats.tasks.total}
subtitle={`${stats.tasks.completed} completed • ${stats.tasks.pending} pending`}
trend={trends.tasks}
icon={<FileTextIcon className="size-6" />}
accentColor="blue"
href="/writer/tasks"
details={[
{ label: "Total Tasks", value: stats.tasks.total },
{ label: "Completed", value: stats.tasks.completed },
{ label: "Pending", value: stats.tasks.pending },
{ label: "In Progress", value: stats.tasks.inProgress },
{ label: "Avg Word Count", value: stats.tasks.avgWordCount },
]}
/>
<EnhancedMetricCard
title="Content Pieces"
value={stats.content.total}
subtitle={`${stats.content.published} published • ${stats.content.drafts} drafts`}
trend={trends.content}
icon={<PencilIcon className="size-6" />}
accentColor="green"
href="/writer/content"
details={[
{ label: "Total Content", value: stats.content.total },
{ label: "Published", value: stats.content.published },
{ label: "In Review", value: stats.content.review },
{ label: "Drafts", value: stats.content.drafts },
{ label: "Avg Word Count", value: stats.content.avgWordCount.toLocaleString() },
]}
/>
<EnhancedMetricCard
title="Images Generated"
value={stats.images.generated}
subtitle={`${stats.images.total} total • ${stats.images.pending} pending`}
trend={trends.images}
icon={<BoxIcon className="size-6" />}
accentColor="orange"
href="/writer/images"
details={[
{ label: "Generated", value: stats.images.generated },
{ label: "Total Images", value: stats.images.total },
{ label: "Pending", value: stats.images.pending },
{ label: "Failed", value: stats.images.failed },
]}
/>
<EnhancedMetricCard
title="Publish Rate"
value={`${stats.productivity.publishRate}%`}
subtitle={`${stats.content.published} of ${stats.content.total} published`}
icon={<BoltIcon className="size-6" />}
accentColor="purple"
href="/writer/published"
details={[
{ label: "Publish Rate", value: `${stats.productivity.publishRate}%` },
{ label: "Published", value: stats.content.published },
{ label: "Total Content", value: stats.content.total },
{ label: "This Week", value: stats.productivity.contentThisWeek },
{ label: "This Month", value: stats.productivity.contentThisMonth },
]}
/>
</div>
{/* Interactive Workflow Pipeline */}
<ComponentCard title="Writer Workflow Pipeline" desc="Track your content creation progress through each stage">
<WorkflowPipeline
steps={workflowSteps.map(step => ({
number: step.number,
title: step.title,
status: step.status === "completed" ? "completed" : step.status === "in_progress" ? "in_progress" : "pending",
count: step.count || 0,
path: step.path,
description: step.title,
details: step.status === "completed"
? `${step.title} completed with ${step.count} items`
: step.status === "pending"
? `${step.title} pending - ${step.count} items ready`
: `${step.title} in progress`,
}))}
onStepClick={(step) => {
navigate(step.path);
}}
showConnections={true}
/>
</ComponentCard>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Productivity Metrics */}
<ComponentCard title="Productivity Metrics" desc="Content creation performance" 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">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 className="pt-4 border-t border-gray-200 dark:border-gray-800">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">This Week</p>
<p className="text-lg font-bold text-gray-800 dark:text-white/90">{stats.productivity.contentThisWeek}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">This Month</p>
<p className="text-lg font-bold text-gray-800 dark:text-white/90">{stats.productivity.contentThisMonth}</p>
</div>
</div>
</div>
<div className="pt-4 border-t border-gray-200 dark:border-gray-800">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Avg Word Count</p>
<p className="text-lg font-bold text-gray-800 dark:text-white/90">
{stats.content.avgWordCount.toLocaleString()}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.content.totalWordCount.toLocaleString()} total words
</p>
</div>
</div>
</div>
</ComponentCard>
{/* Content Status Chart */}
{contentStatusChart && (
<ComponentCard title="Content by Status" desc="Distribution across workflow stages" className="lg:col-span-2">
<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>}>
<Chart
options={contentStatusChart.options}
series={contentStatusChart.series}
type="bar"
height={300}
/>
</Suspense>
</ComponentCard>
)}
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Tasks by Status */}
{tasksStatusChart && (
<ComponentCard title="Tasks by Status" desc="Task distribution across statuses">
<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>}>
<Chart
options={tasksStatusChart.options}
series={tasksStatusChart.series}
type="donut"
height={300}
/>
</Suspense>
</ComponentCard>
)}
{/* Images by Type */}
{imagesTypeChart ? (
<ComponentCard title="Images by Type" desc="Image generation breakdown">
<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>}>
<Chart
options={imagesTypeChart.options}
series={imagesTypeChart.series}
type="bar"
height={300}
/>
</Suspense>
</ComponentCard>
) : (
<ComponentCard title="Images Overview" desc="Image generation status">
<div className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-900/50">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Generated</span>
<span className="text-lg font-bold text-success-500">{stats.images.generated}</span>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-900/50">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Pending</span>
<span className="text-lg font-bold text-warning-500">{stats.images.pending}</span>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-900/50">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Failed</span>
<span className="text-lg font-bold text-error-500">{stats.images.failed}</span>
</div>
</div>
</ComponentCard>
)}
</div>
{/* Productivity Chart */}
{productivityChart && (
<ComponentCard title="Content Creation Trend" desc="Content created this week and month">
<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>}>
<Chart
options={productivityChart.options}
series={productivityChart.series}
type="area"
height={200}
/>
</Suspense>
</ComponentCard>
)}
{/* Next Actions */}
{nextActions.length > 0 && (
<ComponentCard title="Next Actions" desc="Actionable items requiring attention">
<div className="space-y-3">
{nextActions.map((action, index) => (
<div
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"
>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{action.text}</span>
<Link
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"
>
{action.action}
<ArrowRightIcon className="size-4" />
</Link>
</div>
))}
</div>
</ComponentCard>
)}
</div>
</>
);
}