- Ajout de type:user_role dans le tag GORM du champ Role - Amélioration de la détection d'erreurs ENUM dans le service Register - L'endpoint /auth/register retourne maintenant 201 OK avec tokens - Score production: 52/70 → 58/70 - PROD-010 marqué comme fixed (P0 blocker résolu)
265 lines
5.6 KiB
TypeScript
265 lines
5.6 KiB
TypeScript
import React from 'react';
|
|
import { Modal } from './modal';
|
|
import { Button } from './button';
|
|
import { cn } from '@/lib/utils';
|
|
import { AlertCircle, Info } from 'lucide-react';
|
|
|
|
export interface DialogProps {
|
|
open: boolean;
|
|
onClose?: () => void;
|
|
onOpenChange?: (open: boolean) => void;
|
|
title?: string;
|
|
children: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
variant?: 'default' | 'alert' | 'confirm' | 'info';
|
|
onConfirm?: () => void | Promise<void>;
|
|
onCancel?: () => void;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
showCancel?: boolean;
|
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
}
|
|
|
|
const variantIcons = {
|
|
alert: AlertCircle,
|
|
confirm: AlertCircle,
|
|
info: Info,
|
|
default: undefined,
|
|
};
|
|
|
|
const variantStyles = {
|
|
alert: 'text-destructive',
|
|
confirm: 'text-primary',
|
|
info: 'text-blue-600',
|
|
default: '',
|
|
};
|
|
|
|
/**
|
|
* Composant Dialog avancé avec header, body, footer et actions.
|
|
*/
|
|
export function Dialog({
|
|
open,
|
|
onClose,
|
|
onOpenChange,
|
|
title,
|
|
children,
|
|
footer,
|
|
variant = 'default',
|
|
onConfirm,
|
|
onCancel,
|
|
confirmLabel = 'Confirm',
|
|
cancelLabel = 'Cancel',
|
|
showCancel = true,
|
|
size = 'md',
|
|
}: DialogProps) {
|
|
const handleClose = () => {
|
|
if (onOpenChange) {
|
|
onOpenChange(false);
|
|
} else if (onClose) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
const handleConfirm = async () => {
|
|
if (onConfirm) {
|
|
await onConfirm();
|
|
}
|
|
handleClose();
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
if (onCancel) {
|
|
onCancel();
|
|
}
|
|
handleClose();
|
|
};
|
|
|
|
const IconComponent = variantIcons[variant];
|
|
const iconStyle = variantStyles[variant];
|
|
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
onClose={handleClose}
|
|
size={size}
|
|
closeOnOverlayClick={variant === 'default'}
|
|
>
|
|
<div className="flex flex-col">
|
|
{/* Header */}
|
|
{title && (
|
|
<DialogHeader variant={variant}>
|
|
<div className="flex items-center gap-3">
|
|
{IconComponent && (
|
|
<IconComponent className={cn('h-5 w-5', iconStyle)} />
|
|
)}
|
|
<h2 className="text-2xl font-semibold leading-none tracking-tight">
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
</DialogHeader>
|
|
)}
|
|
|
|
{/* Body */}
|
|
<DialogBody variant={variant}>{children}</DialogBody>
|
|
|
|
{/* Footer */}
|
|
{footer || onConfirm || onCancel ? (
|
|
<DialogFooter>
|
|
{footer ? (
|
|
footer
|
|
) : (
|
|
<div className="flex justify-end gap-2">
|
|
{showCancel && (
|
|
<Button variant="outline" onClick={handleCancel}>
|
|
{cancelLabel}
|
|
</Button>
|
|
)}
|
|
{onConfirm && (
|
|
<Button
|
|
variant={variant === 'alert' ? 'destructive' : 'default'}
|
|
onClick={handleConfirm}
|
|
>
|
|
{confirmLabel}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</DialogFooter>
|
|
) : null}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export interface DialogHeaderProps {
|
|
children: React.ReactNode;
|
|
variant?: 'default' | 'alert' | 'confirm' | 'info';
|
|
className?: string;
|
|
}
|
|
|
|
export function DialogHeader({
|
|
children,
|
|
variant: _variant = 'default', // Unused but kept for API compatibility
|
|
className,
|
|
}: DialogHeaderProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-between p-6 border-b',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface DialogBodyProps {
|
|
children: React.ReactNode;
|
|
variant?: 'default' | 'alert' | 'confirm' | 'info';
|
|
className?: string;
|
|
}
|
|
|
|
export function DialogBody({
|
|
children,
|
|
variant = 'default',
|
|
className,
|
|
}: DialogBodyProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'p-6',
|
|
variant === 'alert' && 'text-destructive-foreground',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface DialogFooterProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function DialogFooter({ children, className }: DialogFooterProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-end gap-2 p-6 border-t',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Radix UI-style components for compatibility
|
|
export interface DialogContentProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function DialogContent({ children, className }: DialogContentProps) {
|
|
return <div className={cn('p-6', className)}>{children}</div>;
|
|
}
|
|
|
|
export interface DialogDescriptionProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function DialogDescription({
|
|
children,
|
|
className,
|
|
}: DialogDescriptionProps) {
|
|
return (
|
|
<p className={cn('text-sm text-muted-foreground', className)}>
|
|
{children}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
export interface DialogTitleProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function DialogTitle({ children, className }: DialogTitleProps) {
|
|
return (
|
|
<h2
|
|
className={cn(
|
|
'text-2xl font-semibold leading-none tracking-tight',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</h2>
|
|
);
|
|
}
|
|
|
|
export interface DialogTriggerProps {
|
|
children: React.ReactNode;
|
|
asChild?: boolean;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
export function DialogTrigger({
|
|
children,
|
|
asChild,
|
|
onClick,
|
|
}: DialogTriggerProps) {
|
|
// If asChild, we expect the child to handle the click
|
|
if (asChild && React.isValidElement(children)) {
|
|
return React.cloneElement(children, {
|
|
onClick: onClick || children.props.onClick,
|
|
} as any);
|
|
}
|
|
return (
|
|
<div onClick={onClick} style={{ display: 'inline-block' }}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|