165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
/**
|
|
* NeedsAttentionBar - Compact alert bar for items needing user attention
|
|
*
|
|
* Shows at the top of dashboard when there are:
|
|
* - Content pending review
|
|
* - WordPress sync failures
|
|
* - Incomplete site setup
|
|
* - Automation failures
|
|
*
|
|
* Collapsible and only visible when there are items to show.
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { AlertIcon, ArrowRightIcon, ChevronDownIcon, RefreshIcon, CloseIcon } from '../../icons';
|
|
|
|
export type AttentionType = 'pending_review' | 'sync_failed' | 'setup_incomplete' | 'automation_failed' | 'credits_low';
|
|
|
|
export interface AttentionItem {
|
|
id: string;
|
|
type: AttentionType;
|
|
title: string;
|
|
count?: number;
|
|
actionLabel: string;
|
|
actionUrl?: string;
|
|
onAction?: () => void;
|
|
onRetry?: () => void;
|
|
severity: 'warning' | 'error' | 'info';
|
|
}
|
|
|
|
interface NeedsAttentionBarProps {
|
|
items: AttentionItem[];
|
|
onDismiss?: (id: string) => void;
|
|
className?: string;
|
|
}
|
|
|
|
const severityStyles = {
|
|
warning: {
|
|
bg: 'bg-amber-50 dark:bg-amber-500/10',
|
|
border: 'border-amber-200 dark:border-amber-500/30',
|
|
icon: 'text-amber-500',
|
|
text: 'text-amber-800 dark:text-amber-200',
|
|
button: 'bg-amber-100 hover:bg-amber-200 text-amber-700 dark:bg-amber-500/20 dark:hover:bg-amber-500/30 dark:text-amber-200',
|
|
},
|
|
error: {
|
|
bg: 'bg-red-50 dark:bg-red-500/10',
|
|
border: 'border-red-200 dark:border-red-500/30',
|
|
icon: 'text-red-500',
|
|
text: 'text-red-800 dark:text-red-200',
|
|
button: 'bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-500/20 dark:hover:bg-red-500/30 dark:text-red-200',
|
|
},
|
|
info: {
|
|
bg: 'bg-blue-50 dark:bg-blue-500/10',
|
|
border: 'border-blue-200 dark:border-blue-500/30',
|
|
icon: 'text-blue-500',
|
|
text: 'text-blue-800 dark:text-blue-200',
|
|
button: 'bg-blue-100 hover:bg-blue-200 text-blue-700 dark:bg-blue-500/20 dark:hover:bg-blue-500/30 dark:text-blue-200',
|
|
},
|
|
};
|
|
|
|
export default function NeedsAttentionBar({ items, onDismiss, className = '' }: NeedsAttentionBarProps) {
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
|
|
if (items.length === 0) return null;
|
|
|
|
// Group items by severity for display priority
|
|
const errorItems = items.filter(i => i.severity === 'error');
|
|
const warningItems = items.filter(i => i.severity === 'warning');
|
|
const infoItems = items.filter(i => i.severity === 'info');
|
|
const sortedItems = [...errorItems, ...warningItems, ...infoItems];
|
|
|
|
const totalCount = items.reduce((sum, item) => sum + (item.count || 1), 0);
|
|
|
|
return (
|
|
<div className={`mb-6 ${className}`}>
|
|
{/* Header bar - always visible */}
|
|
<button
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
className="w-full flex items-center justify-between p-3 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 rounded-lg hover:bg-amber-100 dark:hover:bg-amber-500/15 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<AlertIcon className="w-5 h-5 text-amber-500" />
|
|
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
|
{totalCount} item{totalCount !== 1 ? 's' : ''} need{totalCount === 1 ? 's' : ''} attention
|
|
</span>
|
|
</div>
|
|
<ChevronDownIcon
|
|
className={`w-5 h-5 text-amber-500 transition-transform ${isCollapsed ? '' : 'rotate-180'}`}
|
|
/>
|
|
</button>
|
|
|
|
{/* Expandable content */}
|
|
{!isCollapsed && (
|
|
<div className="mt-2 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
|
{sortedItems.map((item) => {
|
|
const styles = severityStyles[item.severity];
|
|
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className={`flex items-center justify-between p-3 rounded-lg border ${styles.bg} ${styles.border}`}
|
|
>
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<AlertIcon className={`w-4 h-4 flex-shrink-0 ${styles.icon}`} />
|
|
<span className={`text-sm font-medium truncate ${styles.text}`}>
|
|
{item.count ? `${item.count} ` : ''}{item.title}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 flex-shrink-0 ml-2">
|
|
{item.onRetry && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
item.onRetry?.();
|
|
}}
|
|
className={`p-1.5 rounded ${styles.button} transition-colors`}
|
|
title="Retry"
|
|
>
|
|
<RefreshIcon className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
|
|
{item.actionUrl ? (
|
|
<Link
|
|
to={item.actionUrl}
|
|
className={`px-2 py-1 text-xs font-medium rounded ${styles.button} transition-colors flex items-center gap-1`}
|
|
>
|
|
{item.actionLabel}
|
|
<ArrowRightIcon className="w-3 h-3" />
|
|
</Link>
|
|
) : item.onAction ? (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
item.onAction?.();
|
|
}}
|
|
className={`px-2 py-1 text-xs font-medium rounded ${styles.button} transition-colors`}
|
|
>
|
|
{item.actionLabel}
|
|
</button>
|
|
) : null}
|
|
|
|
{onDismiss && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDismiss(item.id);
|
|
}}
|
|
className="p-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
|
title="Dismiss"
|
|
>
|
|
<CloseIcon className="w-3.5 h-3.5 text-gray-400" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|