Refactor components and modals
This commit is contained in:
321
frontend/src/components/ui/DialogModal/DialogModal.tsx
Normal file
321
frontend/src/components/ui/DialogModal/DialogModal.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LucideIcon, AlertTriangle, CheckCircle, XCircle, Info, X } from 'lucide-react';
|
||||
import Modal, { ModalHeader, ModalBody, ModalFooter } from '../Modal/Modal';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface DialogModalAction {
|
||||
label: string;
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'danger';
|
||||
onClick: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export interface DialogModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
// Content
|
||||
title: string;
|
||||
message: string | React.ReactNode;
|
||||
type?: 'info' | 'warning' | 'error' | 'success' | 'confirm' | 'custom';
|
||||
icon?: LucideIcon;
|
||||
|
||||
// Actions
|
||||
actions?: DialogModalAction[];
|
||||
showCloseButton?: boolean;
|
||||
|
||||
// Convenience props for common dialogs
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
onCancel?: () => void;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
|
||||
// Layout
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DialogModal - Standardized component for simple confirmation dialogs and alerts
|
||||
*
|
||||
* Features:
|
||||
* - Predefined dialog types with appropriate icons and styling
|
||||
* - Support for custom actions or convenient confirm/cancel pattern
|
||||
* - Responsive design optimized for mobile
|
||||
* - Built-in loading states
|
||||
* - Accessibility features
|
||||
*/
|
||||
export const DialogModal: React.FC<DialogModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
message,
|
||||
type = 'info',
|
||||
icon,
|
||||
actions,
|
||||
showCloseButton = true,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
confirmLabel,
|
||||
cancelLabel,
|
||||
size = 'sm',
|
||||
loading = false,
|
||||
}) => {
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
// Default labels with translation fallbacks
|
||||
const defaultConfirmLabel = confirmLabel || t('common:modals.actions.confirm', 'Confirmar');
|
||||
const defaultCancelLabel = cancelLabel || t('common:modals.actions.cancel', 'Cancelar');
|
||||
|
||||
// Get icon and colors based on dialog type
|
||||
const getDialogConfig = () => {
|
||||
if (icon) {
|
||||
return { icon, color: 'var(--text-primary)' };
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return { icon: AlertTriangle, color: '#f59e0b' }; // yellow-500
|
||||
case 'error':
|
||||
return { icon: XCircle, color: '#ef4444' }; // red-500
|
||||
case 'success':
|
||||
return { icon: CheckCircle, color: '#10b981' }; // emerald-500
|
||||
case 'confirm':
|
||||
return { icon: AlertTriangle, color: '#f59e0b' }; // yellow-500
|
||||
case 'info':
|
||||
default:
|
||||
return { icon: Info, color: '#3b82f6' }; // blue-500
|
||||
}
|
||||
};
|
||||
|
||||
const { icon: DialogIcon, color } = getDialogConfig();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (onConfirm) {
|
||||
await onConfirm();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Generate actions based on type and props
|
||||
const getActions = (): DialogModalAction[] => {
|
||||
if (actions) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
// For simple info/success/error dialogs, just show OK button
|
||||
if (type === 'info' || type === 'success' || type === 'error') {
|
||||
return [
|
||||
{
|
||||
label: t('common:modals.actions.ok', 'OK'),
|
||||
variant: 'primary',
|
||||
onClick: onClose,
|
||||
disabled: loading,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// For confirm/warning dialogs, show cancel and confirm buttons
|
||||
if (type === 'confirm' || type === 'warning') {
|
||||
return [
|
||||
{
|
||||
label: defaultCancelLabel,
|
||||
variant: 'outline',
|
||||
onClick: handleCancel,
|
||||
disabled: loading,
|
||||
},
|
||||
{
|
||||
label: defaultConfirmLabel,
|
||||
variant: type === 'warning' ? 'danger' : 'primary',
|
||||
onClick: handleConfirm,
|
||||
disabled: loading,
|
||||
loading: loading,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Default: just OK button
|
||||
return [
|
||||
{
|
||||
label: 'OK',
|
||||
variant: 'primary',
|
||||
onClick: onClose,
|
||||
disabled: loading,
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const dialogActions = getActions();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size={size}
|
||||
closeOnOverlayClick={!loading}
|
||||
closeOnEscape={!loading}
|
||||
showCloseButton={false}
|
||||
>
|
||||
<ModalHeader
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Dialog icon */}
|
||||
<div
|
||||
className="flex-shrink-0 p-2 rounded-lg"
|
||||
style={{ backgroundColor: `${color}15` }}
|
||||
>
|
||||
<DialogIcon
|
||||
className="w-6 h-6"
|
||||
style={{ color }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
showCloseButton={showCloseButton && !loading}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
<ModalBody padding="lg">
|
||||
{loading && (
|
||||
<div className="absolute inset-0 bg-[var(--bg-primary)]/80 backdrop-blur-sm flex items-center justify-center z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[var(--color-primary)]"></div>
|
||||
<span className="text-[var(--text-secondary)]">{t('common:modals.processing', 'Procesando...')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-[var(--text-primary)]">
|
||||
{typeof message === 'string' ? (
|
||||
<p className="leading-relaxed">{message}</p>
|
||||
) : (
|
||||
message
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
{dialogActions.length > 0 && (
|
||||
<ModalFooter justify="end">
|
||||
<div className="flex gap-3 w-full sm:w-auto">
|
||||
{dialogActions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={action.variant || 'outline'}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled || loading}
|
||||
className={`${
|
||||
dialogActions.length > 1 && size === 'xs' ? 'flex-1 sm:flex-none' : ''
|
||||
} min-w-[80px]`}
|
||||
>
|
||||
{action.loading ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
||||
) : (
|
||||
action.label
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// Convenience functions for common dialog types
|
||||
export const showInfoDialog = (
|
||||
title: string,
|
||||
message: string,
|
||||
onClose: () => void
|
||||
): React.ReactElement => (
|
||||
<DialogModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
message={message}
|
||||
type="info"
|
||||
/>
|
||||
);
|
||||
|
||||
export const showWarningDialog = (
|
||||
title: string,
|
||||
message: string,
|
||||
onConfirm: () => void,
|
||||
onCancel?: () => void
|
||||
): React.ReactElement => (
|
||||
<DialogModal
|
||||
isOpen={true}
|
||||
onClose={onCancel || (() => {})}
|
||||
title={title}
|
||||
message={message}
|
||||
type="warning"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
);
|
||||
|
||||
export const showErrorDialog = (
|
||||
title: string,
|
||||
message: string,
|
||||
onClose: () => void
|
||||
): React.ReactElement => (
|
||||
<DialogModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
message={message}
|
||||
type="error"
|
||||
/>
|
||||
);
|
||||
|
||||
export const showSuccessDialog = (
|
||||
title: string,
|
||||
message: string,
|
||||
onClose: () => void
|
||||
): React.ReactElement => (
|
||||
<DialogModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
message={message}
|
||||
type="success"
|
||||
/>
|
||||
);
|
||||
|
||||
export const showConfirmDialog = (
|
||||
title: string,
|
||||
message: string,
|
||||
onConfirm: () => void,
|
||||
onCancel?: () => void,
|
||||
confirmLabel?: string,
|
||||
cancelLabel?: string
|
||||
): React.ReactElement => (
|
||||
<DialogModal
|
||||
isOpen={true}
|
||||
onClose={onCancel || (() => {})}
|
||||
title={title}
|
||||
message={message}
|
||||
type="confirm"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
confirmLabel={confirmLabel}
|
||||
cancelLabel={cancelLabel}
|
||||
/>
|
||||
);
|
||||
|
||||
export default DialogModal;
|
||||
14
frontend/src/components/ui/DialogModal/index.ts
Normal file
14
frontend/src/components/ui/DialogModal/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export {
|
||||
DialogModal,
|
||||
default,
|
||||
showInfoDialog,
|
||||
showWarningDialog,
|
||||
showErrorDialog,
|
||||
showSuccessDialog,
|
||||
showConfirmDialog
|
||||
} from './DialogModal';
|
||||
|
||||
export type {
|
||||
DialogModalProps,
|
||||
DialogModalAction
|
||||
} from './DialogModal';
|
||||
Reference in New Issue
Block a user