184 lines
4.2 KiB
TypeScript
184 lines
4.2 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
};
|