2025-04-21 14:51:08 -07:00

185 lines
5.2 KiB
TypeScript

"use client";
import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
import { cn } from '@/lib/utils';
interface AlertInstanceProps {
id: string;
type: 'error' | 'success' | 'info' | 'upload' | 'warning';
title?: string;
message: string;
duration?: number;
dismissible?: boolean;
onDismiss: (id: string) => void;
}
const AlertInstance = ({
id,
type,
title,
message,
dismissible = true,
onDismiss,
}: AlertInstanceProps) => {
return (
<Alert
className={cn(
"relative mb-2 shadow-md max-w-sm",
type === 'error' && "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
type === 'upload' && "bg-blue-50 text-blue-700 border-blue-200",
type === 'success' && "bg-green-50 text-green-700 border-green-200",
type === 'info' && "bg-gray-50 text-gray-700 border-gray-200",
type === 'warning' && "bg-amber-50 text-amber-700 border-amber-200"
)}
>
{dismissible && (
<button
onClick={() => onDismiss(id)}
className="absolute right-2 top-2 rounded-full p-1 hover:bg-black/5"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
)}
{title && <AlertTitle className={dismissible ? 'pr-5' : ''}>{title}</AlertTitle>}
<AlertDescription>{message}</AlertDescription>
</Alert>
);
};
interface AlertSystemProps {
position?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left' | 'top-center' | 'bottom-center';
}
export function AlertSystem({ position = 'bottom-right' }: AlertSystemProps) {
const [alerts, setAlerts] = useState<AlertInstanceProps[]>([]);
// Custom event handlers for adding and removing alerts
useEffect(() => {
const handleAddAlert = (event: Event) => {
const customEvent = event as CustomEvent<{
id?: string;
type: 'error' | 'success' | 'info' | 'upload' | 'warning';
title?: string;
message: string;
duration?: number;
dismissible?: boolean;
}>;
const alert = customEvent.detail;
if (alert) {
const newAlert: AlertInstanceProps = {
...alert,
id: alert.id || Date.now().toString(),
dismissible: alert.dismissible !== false,
onDismiss: removeAlert,
};
setAlerts(prev => [...prev, newAlert]);
// Auto-dismiss after duration if specified
if (alert.duration) {
setTimeout(() => {
removeAlert(newAlert.id);
}, alert.duration);
}
}
};
const handleRemoveAlert = (event: Event) => {
const customEvent = event as CustomEvent<{id: string}>;
const { id } = customEvent.detail;
if (id) {
removeAlert(id);
}
};
window.addEventListener('morphik:alert', handleAddAlert as EventListener);
window.addEventListener('morphik:alert:remove', handleRemoveAlert as EventListener);
return () => {
window.removeEventListener('morphik:alert', handleAddAlert as EventListener);
window.removeEventListener('morphik:alert:remove', handleRemoveAlert as EventListener);
};
}, []);
const removeAlert = (id: string) => {
setAlerts(prev => prev.filter(alert => alert.id !== id));
};
// Position mapping
const positionClasses = {
'top-right': 'fixed top-4 right-4 z-50',
'bottom-right': 'fixed bottom-4 right-4 z-50',
'top-left': 'fixed top-4 left-4 z-50',
'bottom-left': 'fixed bottom-4 left-4 z-50',
'top-center': 'fixed top-4 left-1/2 -translate-x-1/2 z-50',
'bottom-center': 'fixed bottom-4 left-1/2 -translate-x-1/2 z-50',
};
return (
<div className={cn('flex flex-col animate-in fade-in', positionClasses[position])}>
{alerts.map(alert => (
<AlertInstance
key={alert.id}
{...alert}
onDismiss={removeAlert}
/>
))}
</div>
);
}
// Helper function to show alerts programmatically
export const showAlert = (
message: string,
options?: {
type?: 'error' | 'success' | 'info' | 'upload' | 'warning';
title?: string;
duration?: number; // in milliseconds, none means it stays until dismissed
dismissible?: boolean;
id?: string;
}
) => {
const event = new CustomEvent('morphik:alert', {
detail: {
id: options?.id || Date.now().toString(),
type: options?.type || 'info',
title: options?.title,
message,
duration: options?.duration,
dismissible: options?.dismissible !== false,
},
});
window.dispatchEvent(event);
};
// Upload-specific helper for the upload in-progress alert
export const showUploadAlert = (
count: number,
options?: {
dismissible?: boolean;
id?: string;
}
) => {
showAlert(
`Uploading ${count} ${count === 1 ? 'file' : 'files'}...`,
{
type: 'upload',
dismissible: options?.dismissible === undefined ? false : options.dismissible,
id: options?.id || 'upload-alert',
}
);
};
// Helper to remove an alert by ID
export const removeAlert = (id: string) => {
const event = new CustomEvent('morphik:alert:remove', {
detail: { id },
});
window.dispatchEvent(event);
};