@startsimpli/ui 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +327 -0
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +9 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +20 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +66 -0
- package/src/components/email-editor/email-editor.tsx +497 -0
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +51 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +140 -887
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- package/src/components/settings/index.ts +6 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Card } from '../ui/card';
|
|
2
|
+
import { TrendingDown } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface FunnelStage {
|
|
5
|
+
stage: string;
|
|
6
|
+
count: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PipelineFunnelProps {
|
|
11
|
+
stages: FunnelStage[];
|
|
12
|
+
title?: string;
|
|
13
|
+
onStageClick?: (stage: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PipelineFunnel({
|
|
17
|
+
stages,
|
|
18
|
+
title = 'Pipeline Funnel',
|
|
19
|
+
onStageClick,
|
|
20
|
+
}: PipelineFunnelProps) {
|
|
21
|
+
if (!stages || stages.length === 0) {
|
|
22
|
+
return (
|
|
23
|
+
<Card className="p-6">
|
|
24
|
+
<h2 className="text-xl font-bold text-gray-900 mb-6">{title}</h2>
|
|
25
|
+
<div className="text-center py-8 text-gray-500">
|
|
26
|
+
<TrendingDown className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
27
|
+
<p className="text-sm">No pipeline data available</p>
|
|
28
|
+
</div>
|
|
29
|
+
</Card>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const maxCount = Math.max(...stages.map(s => s.count));
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Card className="p-6">
|
|
37
|
+
<h2 className="text-xl font-bold text-gray-900 mb-6">{title}</h2>
|
|
38
|
+
|
|
39
|
+
<div className="space-y-4" role="list" aria-label="Pipeline stages">
|
|
40
|
+
{stages.map((stage, index) => {
|
|
41
|
+
const percentage = maxCount > 0 ? (stage.count / maxCount) * 100 : 0;
|
|
42
|
+
const stagePercentage = stages[0]?.count > 0
|
|
43
|
+
? Math.round((stage.count / stages[0].count) * 100)
|
|
44
|
+
: 0;
|
|
45
|
+
|
|
46
|
+
const defaultColors = [
|
|
47
|
+
'from-accent-400 to-accent-500',
|
|
48
|
+
'from-primary-400 to-primary-500',
|
|
49
|
+
'from-primary-500 to-primary-600',
|
|
50
|
+
'from-primary-600 to-primary-700',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const gradientColor = stage.color || defaultColors[index % defaultColors.length];
|
|
54
|
+
|
|
55
|
+
const nextStage = stages[index + 1];
|
|
56
|
+
const conversionRate = nextStage
|
|
57
|
+
? Math.round((nextStage.count / stage.count) * 100)
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
key={stage.stage}
|
|
63
|
+
role="listitem"
|
|
64
|
+
className="group"
|
|
65
|
+
onClick={() => onStageClick?.(stage.stage)}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-center justify-between mb-2">
|
|
68
|
+
<span className="text-sm font-medium text-gray-700">
|
|
69
|
+
{stage.stage}
|
|
70
|
+
</span>
|
|
71
|
+
<div className="flex items-center gap-3">
|
|
72
|
+
<span className="text-xs text-gray-500">
|
|
73
|
+
{stagePercentage}% of total
|
|
74
|
+
</span>
|
|
75
|
+
<span className="text-sm font-bold text-gray-900">
|
|
76
|
+
{stage.count.toLocaleString()}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="relative h-8 bg-gray-100 rounded-md overflow-hidden">
|
|
82
|
+
<button
|
|
83
|
+
className={`
|
|
84
|
+
absolute inset-y-0 left-0 bg-gradient-to-r ${gradientColor}
|
|
85
|
+
transition-all duration-300 group-hover:opacity-90
|
|
86
|
+
${onStageClick ? 'cursor-pointer' : 'cursor-default'}
|
|
87
|
+
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
|
|
88
|
+
`}
|
|
89
|
+
style={{ width: `${percentage}%` }}
|
|
90
|
+
aria-label={`${stage.stage}: ${stage.count} prospects, ${stagePercentage}% of total`}
|
|
91
|
+
disabled={!onStageClick}
|
|
92
|
+
>
|
|
93
|
+
<span className="sr-only">
|
|
94
|
+
{stage.stage}: {stage.count} prospects
|
|
95
|
+
</span>
|
|
96
|
+
</button>
|
|
97
|
+
|
|
98
|
+
{conversionRate !== null && (
|
|
99
|
+
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
|
100
|
+
<span className="text-xs font-medium text-white bg-gray-900 bg-opacity-75 px-2 py-1 rounded">
|
|
101
|
+
{conversionRate}% convert to next stage
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{stages.length >= 2 && (
|
|
112
|
+
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
113
|
+
<div className="flex items-center justify-between text-sm">
|
|
114
|
+
<span className="text-gray-600">Overall Conversion</span>
|
|
115
|
+
<span className="font-bold text-gray-900">
|
|
116
|
+
{Math.round((stages[stages.length - 1].count / stages[0].count) * 100)}%
|
|
117
|
+
<span className="text-xs text-gray-500 ml-1">
|
|
118
|
+
({stages[0].stage} → {stages[stages.length - 1].stage})
|
|
119
|
+
</span>
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</Card>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SparklineTrendProps {
|
|
4
|
+
data: number[]
|
|
5
|
+
color?: string
|
|
6
|
+
height?: number
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Map common Tailwind color class suffixes to hex values
|
|
11
|
+
const COLOR_MAP: Record<string, string> = {
|
|
12
|
+
'blue-500': '#3B82F6',
|
|
13
|
+
'green-500': '#10B981',
|
|
14
|
+
'purple-500': '#A855F7',
|
|
15
|
+
'emerald-500': '#10B981',
|
|
16
|
+
'red-500': '#EF4444',
|
|
17
|
+
'orange-500': '#F97316',
|
|
18
|
+
'yellow-500': '#EAB308',
|
|
19
|
+
'indigo-500': '#6366F1',
|
|
20
|
+
'pink-500': '#EC4899',
|
|
21
|
+
'primary-500': '#3B82F6',
|
|
22
|
+
'primary-600': '#2563EB',
|
|
23
|
+
'accent-500': '#A855F7',
|
|
24
|
+
'success-500': '#10B981',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveColor(color: string): string {
|
|
28
|
+
// If it looks like a hex/rgb value already, use it directly
|
|
29
|
+
if (color.startsWith('#') || color.startsWith('rgb')) return color
|
|
30
|
+
|
|
31
|
+
// Strip common Tailwind prefixes like "text-"
|
|
32
|
+
const key = color.replace(/^text-/, '')
|
|
33
|
+
return COLOR_MAP[key] || COLOR_MAP['blue-500']
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SparklineTrend({
|
|
37
|
+
data,
|
|
38
|
+
color = 'blue-500',
|
|
39
|
+
height = 40,
|
|
40
|
+
className,
|
|
41
|
+
}: SparklineTrendProps) {
|
|
42
|
+
if (data.length < 2) return null
|
|
43
|
+
|
|
44
|
+
const width = 100
|
|
45
|
+
const padding = 2
|
|
46
|
+
|
|
47
|
+
const min = Math.min(...data)
|
|
48
|
+
const max = Math.max(...data)
|
|
49
|
+
const range = max - min || 1
|
|
50
|
+
|
|
51
|
+
const points = data.map((value, index) => {
|
|
52
|
+
const x = (index / (data.length - 1)) * width
|
|
53
|
+
const y = height - ((value - min) / range) * (height - padding * 2) - padding
|
|
54
|
+
return `${x},${y}`
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const pathData = `M ${points.join(' L ')}`
|
|
58
|
+
const areaData = `${pathData} L ${width},${height} L 0,${height} Z`
|
|
59
|
+
|
|
60
|
+
const hexColor = resolveColor(color)
|
|
61
|
+
const gradientId = `sparkline-gradient-${React.useId().replace(/:/g, '')}`
|
|
62
|
+
|
|
63
|
+
const trend =
|
|
64
|
+
data[data.length - 1] > data[0]
|
|
65
|
+
? 'increasing'
|
|
66
|
+
: data[data.length - 1] < data[0]
|
|
67
|
+
? 'decreasing'
|
|
68
|
+
: 'stable'
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<svg
|
|
72
|
+
width="100%"
|
|
73
|
+
height={height}
|
|
74
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
75
|
+
preserveAspectRatio="none"
|
|
76
|
+
className={className ?? 'w-full'}
|
|
77
|
+
role="img"
|
|
78
|
+
aria-label={`Trend chart showing ${trend} pattern`}
|
|
79
|
+
>
|
|
80
|
+
<defs>
|
|
81
|
+
<linearGradient id={gradientId} x1="0%" y1="0%" x2="0%" y2="100%">
|
|
82
|
+
<stop offset="0%" stopColor={hexColor} stopOpacity="0.3" />
|
|
83
|
+
<stop offset="100%" stopColor={hexColor} stopOpacity="0.05" />
|
|
84
|
+
</linearGradient>
|
|
85
|
+
</defs>
|
|
86
|
+
<path
|
|
87
|
+
d={areaData}
|
|
88
|
+
fill={`url(#${gradientId})`}
|
|
89
|
+
className="transition-all duration-300"
|
|
90
|
+
/>
|
|
91
|
+
<path
|
|
92
|
+
d={pathData}
|
|
93
|
+
fill="none"
|
|
94
|
+
stroke={hexColor}
|
|
95
|
+
strokeWidth="1.5"
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeLinejoin="round"
|
|
98
|
+
className="transition-all duration-300"
|
|
99
|
+
/>
|
|
100
|
+
</svg>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Card } from '../ui/card';
|
|
2
|
+
import { EmptyState } from '../states';
|
|
3
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface TopCampaign {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
engagementRate: number;
|
|
9
|
+
status?: 'active' | 'paused' | 'completed';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TopCampaignsProps {
|
|
13
|
+
campaigns: TopCampaign[];
|
|
14
|
+
title?: string;
|
|
15
|
+
maxItems?: number;
|
|
16
|
+
onViewAll?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function TopCampaigns({
|
|
20
|
+
campaigns,
|
|
21
|
+
title = 'Top Performing Campaigns',
|
|
22
|
+
maxItems = 5,
|
|
23
|
+
onViewAll,
|
|
24
|
+
}: TopCampaignsProps) {
|
|
25
|
+
const displayCampaigns = campaigns.slice(0, maxItems);
|
|
26
|
+
|
|
27
|
+
if (campaigns.length === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<Card className="p-6">
|
|
30
|
+
<h2 className="text-xl font-bold text-gray-900 mb-6">{title}</h2>
|
|
31
|
+
<EmptyState
|
|
32
|
+
title="No campaigns yet"
|
|
33
|
+
description="Launch your first campaign to see performance metrics"
|
|
34
|
+
/>
|
|
35
|
+
</Card>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Card className="p-6">
|
|
41
|
+
<div className="flex items-center justify-between mb-6">
|
|
42
|
+
<h2 className="text-xl font-bold text-gray-900">{title}</h2>
|
|
43
|
+
{onViewAll && campaigns.length > maxItems && (
|
|
44
|
+
<button
|
|
45
|
+
onClick={onViewAll}
|
|
46
|
+
className="text-sm text-primary-600 hover:text-primary-700 font-medium flex items-center gap-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded"
|
|
47
|
+
>
|
|
48
|
+
View all
|
|
49
|
+
<ArrowRight className="w-4 h-4" />
|
|
50
|
+
</button>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="space-y-4" role="list" aria-label="Top campaigns by engagement">
|
|
55
|
+
{displayCampaigns.map((campaign, index) => {
|
|
56
|
+
const getProgressColor = (rate: number) => {
|
|
57
|
+
if (rate >= 75) return 'bg-success-500';
|
|
58
|
+
if (rate >= 50) return 'bg-primary-500';
|
|
59
|
+
if (rate >= 25) return 'bg-warning-500';
|
|
60
|
+
return 'bg-gray-400';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const progressColor = getProgressColor(campaign.engagementRate);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
key={campaign.id}
|
|
68
|
+
role="listitem"
|
|
69
|
+
className="group space-y-2"
|
|
70
|
+
>
|
|
71
|
+
<div className="flex items-start justify-between gap-3">
|
|
72
|
+
<div className="flex items-start gap-2 min-w-0 flex-1">
|
|
73
|
+
<span className={`
|
|
74
|
+
flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full text-xs font-bold
|
|
75
|
+
${index === 0 ? 'bg-accent-100 text-accent-700 border border-accent-200' : 'bg-gray-100 text-gray-600'}
|
|
76
|
+
`}>
|
|
77
|
+
{index + 1}
|
|
78
|
+
</span>
|
|
79
|
+
|
|
80
|
+
<div className="min-w-0 flex-1">
|
|
81
|
+
<h3 className="text-sm font-medium text-gray-900 truncate group-hover:text-primary-600 transition-colors">
|
|
82
|
+
{campaign.name}
|
|
83
|
+
</h3>
|
|
84
|
+
{campaign.status && (
|
|
85
|
+
<span className={`
|
|
86
|
+
inline-block text-xs px-2 py-0.5 rounded-full mt-1 border
|
|
87
|
+
${campaign.status === 'active' ? 'bg-primary-50 text-primary-700 border-primary-200' : ''}
|
|
88
|
+
${campaign.status === 'paused' ? 'bg-warning-50 text-warning-700 border-warning-200' : ''}
|
|
89
|
+
${campaign.status === 'completed' ? 'bg-accent-50 text-accent-700 border-accent-200' : ''}
|
|
90
|
+
`}>
|
|
91
|
+
{campaign.status}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<span className="flex-shrink-0 text-sm font-bold text-gray-900">
|
|
98
|
+
{campaign.engagementRate}%
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="relative h-2 bg-gray-100 rounded-full overflow-hidden">
|
|
103
|
+
<div
|
|
104
|
+
className={`absolute inset-y-0 left-0 ${progressColor} transition-all duration-500 rounded-full`}
|
|
105
|
+
style={{ width: `${campaign.engagementRate}%` }}
|
|
106
|
+
role="progressbar"
|
|
107
|
+
aria-valuenow={campaign.engagementRate}
|
|
108
|
+
aria-valuemin={0}
|
|
109
|
+
aria-valuemax={100}
|
|
110
|
+
aria-label={`${campaign.name} engagement rate: ${campaign.engagementRate}%`}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{campaigns.length > 0 && (
|
|
119
|
+
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
120
|
+
<div className="flex items-center justify-between text-sm">
|
|
121
|
+
<span className="text-gray-600">Average Engagement</span>
|
|
122
|
+
<span className="font-bold text-gray-900">
|
|
123
|
+
{Math.round(
|
|
124
|
+
campaigns.reduce((sum, c) => sum + c.engagementRate, 0) / campaigns.length
|
|
125
|
+
)}%
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</Card>
|
|
131
|
+
);
|
|
132
|
+
}
|