398 lines
8.3 KiB
TypeScript
398 lines
8.3 KiB
TypeScript
import React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* DataListProps - Propriétés du composant DataList
|
|
*
|
|
* @interface DataListProps
|
|
* @template T - Type des éléments de la liste
|
|
*/
|
|
interface DataListProps<T> {
|
|
/**
|
|
* Tableau d'éléments à afficher
|
|
*/
|
|
items: T[];
|
|
|
|
/**
|
|
* Fonction pour rendre chaque élément de la liste
|
|
*
|
|
* @param {T} item - Élément à rendre
|
|
* @param {number} index - Index de l'élément dans la liste
|
|
* @returns {React.ReactNode} Élément React à afficher
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <DataList
|
|
* items={users}
|
|
* renderItem={(user) => <div>{user.name}</div>}
|
|
* keyExtractor={(user) => user.id}
|
|
* />
|
|
* ```
|
|
*/
|
|
renderItem: (item: T, index: number) => React.ReactNode;
|
|
|
|
/**
|
|
* Fonction pour extraire une clé unique de chaque élément
|
|
*
|
|
* @param {T} item - Élément
|
|
* @returns {string} Clé unique
|
|
*/
|
|
keyExtractor: (item: T) => string;
|
|
|
|
/**
|
|
* Message à afficher lorsque la liste est vide
|
|
*
|
|
* @default 'No items found'
|
|
*/
|
|
emptyMessage?: string;
|
|
|
|
/**
|
|
* Si `true`, affiche un état de chargement avec des skeletons
|
|
*
|
|
* @default false
|
|
*/
|
|
loading?: boolean;
|
|
|
|
/**
|
|
* Message d'erreur à afficher si une erreur survient
|
|
*/
|
|
error?: string;
|
|
|
|
/**
|
|
* Classes CSS personnalisées pour le conteneur
|
|
*/
|
|
className?: string;
|
|
|
|
/**
|
|
* Classes CSS personnalisées pour chaque élément
|
|
*/
|
|
itemClassName?: string;
|
|
}
|
|
|
|
/**
|
|
* DataList - Composant de liste générique avec gestion des états
|
|
*
|
|
* Composant réutilisable pour afficher des listes d'éléments avec support pour :
|
|
* - État de chargement (skeletons)
|
|
* - État d'erreur
|
|
* - État vide
|
|
* - Rendu personnalisé des éléments
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* // Liste simple
|
|
* <DataList
|
|
* items={users}
|
|
* renderItem={(user) => (
|
|
* <div className="p-4 border">
|
|
* <h3>{user.name}</h3>
|
|
* <p>{user.email}</p>
|
|
* </div>
|
|
* )}
|
|
* keyExtractor={(user) => user.id}
|
|
* />
|
|
* ```
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* // Avec états de chargement et d'erreur
|
|
* <DataList
|
|
* items={tracks}
|
|
* renderItem={(track) => <TrackCard track={track} />}
|
|
* keyExtractor={(track) => track.id}
|
|
* loading={isLoading}
|
|
* error={error}
|
|
* emptyMessage="Aucune piste trouvée"
|
|
* />
|
|
* ```
|
|
*
|
|
* @component
|
|
* @template T - Type des éléments de la liste
|
|
* @param {DataListProps<T>} props - Propriétés du composant
|
|
* @returns {JSX.Element} Liste d'éléments avec gestion des états
|
|
*/
|
|
|
|
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-kodo-slate dark:bg-kodo-steel rounded h-16"
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className={cn('text-center py-8', className)}>
|
|
<p className="text-kodo-red dark:text-kodo-red">{error}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className={cn('text-center py-8', className)}>
|
|
<p className="text-kodo-content-dim dark:text-kodo-content-dim">{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>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* ModalProps - Propriétés du composant Modal
|
|
*
|
|
* @interface ModalProps
|
|
*/
|
|
interface ModalProps {
|
|
/**
|
|
* Si `true`, le modal est ouvert
|
|
*/
|
|
isOpen: boolean;
|
|
|
|
/**
|
|
* Fonction appelée pour fermer le modal
|
|
*/
|
|
onClose: () => void;
|
|
|
|
/**
|
|
* Titre du modal (affiché dans le header)
|
|
*/
|
|
title?: string;
|
|
|
|
/**
|
|
* Contenu du modal
|
|
*/
|
|
children: React.ReactNode;
|
|
|
|
/**
|
|
* Taille du modal
|
|
*
|
|
* - `sm`: max-w-md
|
|
* - `md`: max-w-lg - par défaut
|
|
* - `lg`: max-w-2xl
|
|
* - `xl`: max-w-4xl
|
|
*
|
|
* @default 'md'
|
|
*/
|
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
|
|
/**
|
|
* Si `true`, affiche le bouton de fermeture dans le header
|
|
*
|
|
* @default true
|
|
*/
|
|
showCloseButton?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Modal - Composant de modal avec backdrop et fermeture
|
|
*
|
|
* Composant modal simple avec backdrop cliquable et bouton de fermeture.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <Modal
|
|
* isOpen={isOpen}
|
|
* onClose={() => setIsOpen(false)}
|
|
* title="Titre du modal"
|
|
* >
|
|
* <p>Contenu du modal</p>
|
|
* </Modal>
|
|
* ```
|
|
*
|
|
* @component
|
|
*/
|
|
|
|
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-kodo-graphite 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-kodo-steel dark:border-kodo-steel">
|
|
{title && (
|
|
<h2 className="text-lg font-semibold text-kodo-text-main dark:text-white">
|
|
{title}
|
|
</h2>
|
|
)}
|
|
{showCloseButton && (
|
|
<button
|
|
onClick={onClose}
|
|
className="text-kodo-content-dim hover:text-kodo-content-dim dark:hover:text-kodo-text-main"
|
|
>
|
|
<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>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* DropdownProps - Propriétés du composant Dropdown
|
|
*
|
|
* @interface DropdownProps
|
|
*/
|
|
interface DropdownProps {
|
|
/**
|
|
* Élément déclencheur du dropdown (bouton, etc.)
|
|
*/
|
|
trigger: React.ReactNode;
|
|
|
|
/**
|
|
* Contenu du dropdown (menu items, etc.)
|
|
*/
|
|
children: React.ReactNode;
|
|
|
|
/**
|
|
* Si `true`, le dropdown est ouvert
|
|
*/
|
|
isOpen: boolean;
|
|
|
|
/**
|
|
* Fonction appelée pour basculer l'état ouvert/fermé
|
|
*/
|
|
onToggle: () => void;
|
|
|
|
/**
|
|
* Alignement du dropdown par rapport au trigger
|
|
*
|
|
* - `left`: Aligné à gauche
|
|
* - `right`: Aligné à droite
|
|
*
|
|
* @default 'left'
|
|
*/
|
|
align?: 'left' | 'right';
|
|
|
|
/**
|
|
* Classes CSS personnalisées pour le dropdown
|
|
*/
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Dropdown - Composant de menu déroulant basique
|
|
*
|
|
* Composant de dropdown avec backdrop et positionnement relatif au trigger.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <Dropdown
|
|
* trigger={<Button>Menu</Button>}
|
|
* isOpen={isOpen}
|
|
* onToggle={() => setIsOpen(!isOpen)}
|
|
* align="right"
|
|
* >
|
|
* <div className="p-2">
|
|
* <button>Option 1</button>
|
|
* <button>Option 2</button>
|
|
* </div>
|
|
* </Dropdown>
|
|
* ```
|
|
*
|
|
* @component
|
|
*/
|
|
|
|
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-kodo-graphite rounded-md shadow-lg',
|
|
'border border-kodo-steel dark:border-kodo-steel',
|
|
align === 'right' ? 'right-0' : 'left-0',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|