feat: add clsx and tailwind-merge dependencies, enhance Kanban components
- Added `clsx` and `tailwind-merge` to `package.json` and `package-lock.json` for improved class management and utility merging. - Updated `Column` and `TaskCard` components to utilize new UI components (`Card`, `Badge`) for a more cohesive design. - Refactored styles in `Header` and Kanban components to align with the new design system. - Marked several tasks as completed in `TODO.md` reflecting progress on UI enhancements.
This commit is contained in:
38
TODO.md
38
TODO.md
@@ -25,31 +25,47 @@
|
||||
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
|
||||
|
||||
### 2.1 Système de design et composants UI
|
||||
- [ ] Créer les composants UI de base (Button, Input, Card, Modal, etc.)
|
||||
- [ ] Implémenter le système de design (couleurs, typographie, spacing)
|
||||
- [ ] Setup Tailwind CSS avec design tokens personnalisés
|
||||
- [ ] Créer une palette de couleurs moderne et accessible
|
||||
- [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge)
|
||||
- [x] Implémenter le système de design tech dark (couleurs, typographie, spacing)
|
||||
- [x] Setup Tailwind CSS avec classes utilitaires personnalisées
|
||||
- [x] Créer une palette de couleurs tech/cyberpunk
|
||||
|
||||
### 2.2 Composants Kanban existants (à améliorer)
|
||||
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
|
||||
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
|
||||
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
|
||||
- [x] `components/ui/Header.tsx` - Header avec statistiques
|
||||
- [ ] Améliorer le design et l'UX des composants existants
|
||||
- [x] Refactoriser les composants pour utiliser le nouveau système UI
|
||||
|
||||
### 2.3 Clients HTTP et hooks
|
||||
- [ ] `clients/tasks-client.ts` - Client pour les tâches
|
||||
### 2.3 Gestion des tâches (CRUD)
|
||||
- [ ] Formulaire de création de tâche (Modal + Form)
|
||||
- [ ] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage)
|
||||
- [ ] Suppression de tâche (confirmation + API call)
|
||||
- [ ] Changement de statut par drag & drop ou boutons
|
||||
- [ ] Validation des formulaires et gestion d'erreurs
|
||||
|
||||
### 2.4 Gestion des tags
|
||||
- [ ] Créer/éditer des tags avec sélecteur de couleur
|
||||
- [ ] Autocomplete pour les tags existants
|
||||
- [ ] Suppression de tags (avec vérification des dépendances)
|
||||
- [ ] Affichage des tags avec couleurs personnalisées
|
||||
- [ ] Filtrage par tags
|
||||
|
||||
### 2.5 Clients HTTP et hooks
|
||||
- [ ] `clients/tasks-client.ts` - Client pour les tâches (CRUD)
|
||||
- [ ] `clients/tags-client.ts` - Client pour les tags
|
||||
- [ ] `clients/base/http-client.ts` - Client HTTP de base
|
||||
- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches
|
||||
- [ ] `hooks/useTags.ts` - Hook pour la gestion des tags
|
||||
- [ ] `hooks/useKanban.ts` - Hook pour drag & drop
|
||||
- [ ] Gestion des erreurs et loading states
|
||||
|
||||
### 2.4 Fonctionnalités Kanban avancées
|
||||
### 2.6 Fonctionnalités Kanban avancées
|
||||
- [ ] Drag & drop entre colonnes (react-beautiful-dnd)
|
||||
- [ ] Formulaires de création/édition de tâches
|
||||
- [ ] Filtrage par tags/statut/priorité
|
||||
- [ ] Filtrage par statut/priorité/assigné
|
||||
- [ ] Recherche en temps réel dans les tâches
|
||||
- [ ] Gestion des tags avec couleurs
|
||||
- [ ] Tri des tâches (date, priorité, alphabétique)
|
||||
- [ ] Actions en lot (sélection multiple)
|
||||
|
||||
## 📊 Phase 3: Dashboard et analytics (Priorité 3)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Task, TaskStatus } from '@/lib/types';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
interface KanbanColumnProps {
|
||||
id: TaskStatus;
|
||||
@@ -47,10 +49,12 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||
cancelled: '✕'
|
||||
};
|
||||
|
||||
const badgeVariant = color === 'green' ? 'success' : color === 'blue' ? 'primary' : color === 'red' ? 'danger' : 'default';
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 w-80 h-full">
|
||||
{/* Header tech avec glow */}
|
||||
<div className={`bg-slate-900 ${style.border} border rounded-t-lg p-4 ${style.glow} shadow-lg`}>
|
||||
<Card variant="elevated" className="h-full flex flex-col">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
||||
@@ -58,14 +62,13 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<span className={`${style.badge} px-3 py-1 rounded-full text-xs font-mono font-bold`}>
|
||||
<Badge variant={badgeVariant} size="sm">
|
||||
{String(tasks.length).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* Zone de contenu tech */}
|
||||
<div className={`bg-slate-900/80 backdrop-blur-sm ${style.border} border-t-0 border rounded-b-lg p-4 h-[calc(100vh-220px)] overflow-y-auto ${style.glow} shadow-lg`}>
|
||||
<CardContent className="flex-1 p-4 h-[calc(100vh-220px)] overflow-y-auto">
|
||||
<div className="space-y-3">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
@@ -83,7 +86,8 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Task } from '@/lib/types';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
@@ -12,22 +14,9 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
const emojis = task.title.match(emojiRegex) || [];
|
||||
const titleWithoutEmojis = task.title.replace(emojiRegex, '').trim();
|
||||
|
||||
// Couleur de priorité
|
||||
const priorityColors = {
|
||||
low: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
||||
medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300',
|
||||
high: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
urgent: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
|
||||
};
|
||||
|
||||
// Couleur de source
|
||||
const sourceColors = {
|
||||
reminders: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
jira: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-800/50 backdrop-blur-sm border border-slate-700/50 rounded-lg p-3 hover:bg-slate-800/80 hover:border-cyan-500/30 hover:shadow-lg hover:shadow-cyan-500/10 transition-all duration-300 cursor-pointer group">
|
||||
<Card className="p-3 hover:border-cyan-500/30 hover:shadow-lg hover:shadow-cyan-500/10 transition-all duration-300 cursor-pointer group">
|
||||
{/* Header tech avec titre et status */}
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
{emojis.length > 0 && (
|
||||
@@ -59,21 +48,23 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tags tech style */}
|
||||
{/* Tags avec composant Badge */}
|
||||
{task.tags && task.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{task.tags.slice(0, 3).map((tag, index) => (
|
||||
<span
|
||||
<Badge
|
||||
key={index}
|
||||
className="px-2 py-1 rounded text-xs font-mono font-bold bg-cyan-950/50 text-cyan-300 border border-cyan-500/30 hover:bg-cyan-950/80 transition-colors"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="hover:bg-cyan-950/80 transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
{task.tags.length > 3 && (
|
||||
<span className="px-2 py-1 rounded text-xs font-mono text-slate-500 border border-slate-600">
|
||||
<Badge variant="outline" size="sm">
|
||||
+{task.tags.length - 3}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -98,6 +89,6 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
44
components/ui/Badge.tsx
Normal file
44
components/ui/Badge.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline';
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
({ className, variant = 'default', size = 'md', ...props }, ref) => {
|
||||
const baseStyles = 'inline-flex items-center font-mono font-medium transition-all duration-200';
|
||||
|
||||
const variants = {
|
||||
default: 'bg-slate-800/50 text-slate-300 border border-slate-600/50',
|
||||
primary: 'bg-cyan-950/50 text-cyan-300 border border-cyan-500/30',
|
||||
success: 'bg-emerald-950/50 text-emerald-300 border border-emerald-500/30',
|
||||
warning: 'bg-yellow-950/50 text-yellow-300 border border-yellow-500/30',
|
||||
danger: 'bg-red-950/50 text-red-300 border border-red-500/30',
|
||||
outline: 'bg-transparent text-slate-400 border border-slate-600 hover:bg-slate-800/30 hover:text-slate-300'
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-1.5 py-0.5 text-xs rounded',
|
||||
md: 'px-2 py-1 text-xs rounded-md'
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
baseStyles,
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
export { Badge };
|
||||
43
components/ui/Button.tsx
Normal file
43
components/ui/Button.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', ...props }, ref) => {
|
||||
const baseStyles = 'inline-flex items-center justify-center font-mono font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-950 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-cyan-600 hover:bg-cyan-500 text-white border border-cyan-500/30 shadow-cyan-500/20 shadow-lg hover:shadow-cyan-500/30 focus:ring-cyan-500',
|
||||
secondary: 'bg-slate-800 hover:bg-slate-700 text-slate-100 border border-slate-600 shadow-slate-500/20 shadow-lg hover:shadow-slate-500/30 focus:ring-slate-500',
|
||||
danger: 'bg-red-600 hover:bg-red-500 text-white border border-red-500/30 shadow-red-500/20 shadow-lg hover:shadow-red-500/30 focus:ring-red-500',
|
||||
ghost: 'bg-transparent hover:bg-slate-800/50 text-slate-300 hover:text-slate-100 border border-slate-700/50 hover:border-slate-600 focus:ring-slate-500'
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-3 py-1.5 text-xs rounded-md',
|
||||
md: 'px-4 py-2 text-sm rounded-lg',
|
||||
lg: 'px-6 py-3 text-base rounded-lg'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
baseStyles,
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button };
|
||||
80
components/ui/Card.tsx
Normal file
80
components/ui/Card.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'elevated' | 'bordered';
|
||||
}
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant = 'default', ...props }, ref) => {
|
||||
const variants = {
|
||||
default: 'bg-slate-800/50 border border-slate-700/50',
|
||||
elevated: 'bg-slate-800/80 border border-slate-700/50 shadow-lg shadow-slate-900/20',
|
||||
bordered: 'bg-slate-900/50 border border-cyan-500/30 shadow-cyan-500/10 shadow-lg'
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg backdrop-blur-sm transition-all duration-200',
|
||||
variants[variant],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4 border-b border-slate-700/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-mono font-semibold text-slate-100 tracking-wide', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4 border-t border-slate-700/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter };
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -69,49 +71,25 @@ interface StatCardProps {
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color }: StatCardProps) {
|
||||
const techStyles = {
|
||||
blue: {
|
||||
bg: 'bg-slate-800/50',
|
||||
border: 'border-cyan-500/30',
|
||||
text: 'text-cyan-300',
|
||||
glow: 'shadow-cyan-500/20'
|
||||
},
|
||||
green: {
|
||||
bg: 'bg-slate-800/50',
|
||||
border: 'border-emerald-500/30',
|
||||
text: 'text-emerald-300',
|
||||
glow: 'shadow-emerald-500/20'
|
||||
},
|
||||
yellow: {
|
||||
bg: 'bg-slate-800/50',
|
||||
border: 'border-yellow-500/30',
|
||||
text: 'text-yellow-300',
|
||||
glow: 'shadow-yellow-500/20'
|
||||
},
|
||||
gray: {
|
||||
bg: 'bg-slate-800/50',
|
||||
border: 'border-slate-500/30',
|
||||
text: 'text-slate-300',
|
||||
glow: 'shadow-slate-500/20'
|
||||
},
|
||||
purple: {
|
||||
bg: 'bg-slate-800/50',
|
||||
border: 'border-purple-500/30',
|
||||
text: 'text-purple-300',
|
||||
glow: 'shadow-purple-500/20'
|
||||
}
|
||||
|
||||
const textColors = {
|
||||
blue: 'text-cyan-300',
|
||||
green: 'text-emerald-300',
|
||||
yellow: 'text-yellow-300',
|
||||
gray: 'text-slate-300',
|
||||
purple: 'text-purple-300'
|
||||
};
|
||||
|
||||
const style = techStyles[color];
|
||||
|
||||
return (
|
||||
<div className={`${style.bg} ${style.border} border rounded-lg px-3 py-2 ${style.glow} shadow-lg backdrop-blur-sm hover:${style.border.replace('/30', '/50')} transition-all duration-300`}>
|
||||
<div className={`text-xs font-mono font-bold ${style.text} opacity-75 uppercase tracking-wider`}>
|
||||
<Card variant="elevated" className="px-3 py-2 hover:border-opacity-75 transition-all duration-300">
|
||||
<CardContent className="p-0">
|
||||
<div className={`text-xs font-mono font-bold ${textColors[color]} opacity-75 uppercase tracking-wider`}>
|
||||
{label}
|
||||
</div>
|
||||
<div className={`text-lg font-mono font-bold ${style.text}`}>
|
||||
<div className={`text-lg font-mono font-bold ${textColors[color]}`}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
44
components/ui/Input.tsx
Normal file
44
components/ui/Input.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { InputHTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, label, error, ...props }, ref) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label && (
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
className={cn(
|
||||
'w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg',
|
||||
'text-slate-100 font-mono text-sm placeholder-slate-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50',
|
||||
'hover:border-slate-600/50 transition-all duration-200',
|
||||
'backdrop-blur-sm',
|
||||
error && 'border-red-500/50 focus:ring-red-500/50 focus:border-red-500/50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-xs font-mono text-red-400 flex items-center gap-1">
|
||||
<span className="text-red-500">⚠</span>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
78
components/ui/Modal.tsx
Normal file
78
components/ui/Modal.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
export function Modal({ isOpen, onClose, children, title, size = 'md' }: ModalProps) {
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const sizes = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl'
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className={cn(
|
||||
'relative w-full mx-4 bg-slate-900/95 border border-slate-700/50 rounded-lg shadow-2xl shadow-slate-900/50 backdrop-blur-sm',
|
||||
sizes[size]
|
||||
)}>
|
||||
{/* Header */}
|
||||
{title && (
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-700/50">
|
||||
<h2 className="text-lg font-mono font-semibold text-slate-100 tracking-wide">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-100 transition-colors p-1"
|
||||
>
|
||||
<span className="sr-only">Fermer</span>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
9
lib/utils.ts
Normal file
9
lib/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Utility function to merge Tailwind CSS classes
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -9,12 +9,14 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "15.5.3",
|
||||
"prisma": "^6.16.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"sqlite3": "^5.1.7"
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -2717,6 +2719,15 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@@ -7409,6 +7420,16 @@
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "15.5.3",
|
||||
"prisma": "^6.16.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"sqlite3": "^5.1.7"
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/services/*": ["./services/*"],
|
||||
"@/lib/*": ["./lib/*"]
|
||||
"@/lib/*": ["./lib/*"],
|
||||
"@/components/*": ["./components/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
||||
Reference in New Issue
Block a user