veza/apps/web/src/components/ui/DataList.tsx

184 lines
4.2 KiB
TypeScript
Raw Normal View History

import React from 'react';
import { cn } from '@/lib/utils';
interface DataListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
loading?: boolean;
error?: string;
className?: string;
itemClassName?: string;
}
export function DataList<T>({
items,
renderItem,
keyExtractor,
emptyMessage = 'No items found',
loading = false,
error,
className,
itemClassName,
}: DataListProps<T>) {
if (loading) {
return (
<div className={cn('space-y-2', className)}>
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className="animate-pulse bg-gray-200 dark:bg-gray-700 rounded h-16"
/>
))}
</div>
);
}
if (error) {
return (
<div className={cn('text-center py-8', className)}>
<p className="text-red-500 dark:text-red-400">{error}</p>
</div>
);
}
if (items.length === 0) {
return (
<div className={cn('text-center py-8', className)}>
<p className="text-gray-500 dark:text-gray-400">{emptyMessage}</p>
</div>
);
}
return (
<div className={cn('space-y-2', className)}>
{items.map((item, index) => (
<div key={keyExtractor(item)} className={itemClassName}>
{renderItem(item, index)}
</div>
))}
</div>
);
}
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
showCloseButton?: boolean;
}
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'md',
showCloseButton = true,
}) => {
if (!isOpen) return null;
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black bg-opacity-50"
onClick={onClose}
/>
{/* Modal */}
<div
className={cn(
'relative bg-white dark:bg-gray-800 rounded-lg shadow-xl',
'w-full mx-4',
sizeClasses[size]
)}
>
{/* Header */}
{(title || showCloseButton) && (
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
{title && (
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</h2>
)}
{showCloseButton && (
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
)}
{/* Content */}
<div className="p-6">
{children}
</div>
</div>
</div>
);
};
interface DropdownProps {
trigger: React.ReactNode;
children: React.ReactNode;
isOpen: boolean;
onToggle: () => void;
align?: 'left' | 'right';
className?: string;
}
export const Dropdown: React.FC<DropdownProps> = ({
trigger,
children,
isOpen,
onToggle,
align = 'left',
className,
}) => {
return (
<div className="relative">
<div onClick={onToggle}>
{trigger}
</div>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-10"
onClick={onToggle}
/>
{/* Dropdown */}
<div
className={cn(
'absolute z-20 mt-1 bg-white dark:bg-gray-800 rounded-md shadow-lg',
'border border-gray-200 dark:border-gray-700',
align === 'right' ? 'right-0' : 'left-0',
className
)}
>
{children}
</div>
</>
)}
</div>
);
};