diff --git a/TODO.md b/TODO.md
index 5e2a4d5..6618240 100644
--- a/TODO.md
+++ b/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)
diff --git a/components/kanban/Column.tsx b/components/kanban/Column.tsx
index b1803fb..92145ab 100644
--- a/components/kanban/Column.tsx
+++ b/components/kanban/Column.tsx
@@ -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,43 +49,45 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
cancelled: 'âś•'
};
+ const badgeVariant = color === 'green' ? 'success' : color === 'blue' ? 'primary' : color === 'red' ? 'danger' : 'default';
+
return (
- {/* Header tech avec glow */}
-
-
-
-
- {String(tasks.length).padStart(2, '0')}
-
-
-
-
- {/* Zone de contenu tech */}
-
-
- {tasks.length === 0 ? (
-
-
- {techIcons[id]}
-
-
NO DATA
-
+
+
+
+
- ) : (
- tasks.map((task) => (
-
- ))
- )}
-
-
+
+ {String(tasks.length).padStart(2, '0')}
+
+
+
+
+
+
+ {tasks.length === 0 ? (
+
+
+ {techIcons[id]}
+
+
NO DATA
+
+
+ ) : (
+ tasks.map((task) => (
+
+ ))
+ )}
+
+
+
);
}
diff --git a/components/kanban/TaskCard.tsx b/components/kanban/TaskCard.tsx
index fb66aa5..cf8493e 100644
--- a/components/kanban/TaskCard.tsx
+++ b/components/kanban/TaskCard.tsx
@@ -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 (
-
+
{/* Header tech avec titre et status */}
{emojis.length > 0 && (
@@ -59,21 +48,23 @@ export function TaskCard({ task }: TaskCardProps) {
)}
- {/* Tags tech style */}
+ {/* Tags avec composant Badge */}
{task.tags && task.tags.length > 0 && (
{task.tags.slice(0, 3).map((tag, index) => (
-
{tag}
-
+
))}
{task.tags.length > 3 && (
-
+
+{task.tags.length - 3}
-
+
)}
)}
@@ -98,6 +89,6 @@ export function TaskCard({ task }: TaskCardProps) {
)}
-
+
);
}
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx
new file mode 100644
index 0000000..ee3cace
--- /dev/null
+++ b/components/ui/Badge.tsx
@@ -0,0 +1,44 @@
+import { HTMLAttributes, forwardRef } from 'react';
+import { cn } from '@/lib/utils';
+
+interface BadgeProps extends HTMLAttributes {
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline';
+ size?: 'sm' | 'md';
+}
+
+const Badge = forwardRef(
+ ({ 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 (
+
+ );
+ }
+);
+
+Badge.displayName = 'Badge';
+
+export { Badge };
diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx
new file mode 100644
index 0000000..a0ac7f1
--- /dev/null
+++ b/components/ui/Button.tsx
@@ -0,0 +1,43 @@
+import { ButtonHTMLAttributes, forwardRef } from 'react';
+import { cn } from '@/lib/utils';
+
+interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
+ size?: 'sm' | 'md' | 'lg';
+}
+
+const Button = forwardRef(
+ ({ 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.displayName = 'Button';
+
+export { Button };
diff --git a/components/ui/Card.tsx b/components/ui/Card.tsx
new file mode 100644
index 0000000..0313ed3
--- /dev/null
+++ b/components/ui/Card.tsx
@@ -0,0 +1,80 @@
+import { HTMLAttributes, forwardRef } from 'react';
+import { cn } from '@/lib/utils';
+
+interface CardProps extends HTMLAttributes {
+ variant?: 'default' | 'elevated' | 'bordered';
+}
+
+const Card = forwardRef(
+ ({ 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 (
+
+ );
+ }
+);
+
+Card.displayName = 'Card';
+
+const CardHeader = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+
+CardTitle.displayName = 'CardTitle';
+
+const CardContent = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+
+CardContent.displayName = 'CardContent';
+
+const CardFooter = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardTitle, CardContent, CardFooter };
diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx
index 11736d9..3762f3c 100644
--- a/components/ui/Header.tsx
+++ b/components/ui/Header.tsx
@@ -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 (
-
-
- {label}
-
-
- {value}
-
-
+
+
+
+ {label}
+
+
+ {value}
+
+
+
);
}
diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx
new file mode 100644
index 0000000..76c7ab4
--- /dev/null
+++ b/components/ui/Input.tsx
@@ -0,0 +1,44 @@
+import { InputHTMLAttributes, forwardRef } from 'react';
+import { cn } from '@/lib/utils';
+
+interface InputProps extends InputHTMLAttributes {
+ label?: string;
+ error?: string;
+}
+
+const Input = forwardRef(
+ ({ className, label, error, ...props }, ref) => {
+ return (
+
+ {label && (
+
+ )}
+
+ {error && (
+
+ âš
+ {error}
+
+ )}
+
+ );
+ }
+);
+
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/components/ui/Modal.tsx b/components/ui/Modal.tsx
new file mode 100644
index 0000000..92f19c3
--- /dev/null
+++ b/components/ui/Modal.tsx
@@ -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(
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Header */}
+ {title && (
+
+
+ {title}
+
+
+
+ )}
+
+ {/* Content */}
+
+ {children}
+
+
+
,
+ document.body
+ );
+}
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..ac02527
--- /dev/null
+++ b/lib/utils.ts
@@ -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));
+}
diff --git a/package-lock.json b/package-lock.json
index dd8cbdc..98d9b1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 8fe4e74..fc3696e 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tsconfig.json b/tsconfig.json
index a086fe9..51d95a7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,7 +21,8 @@
"paths": {
"@/*": ["./src/*"],
"@/services/*": ["./services/*"],
- "@/lib/*": ["./lib/*"]
+ "@/lib/*": ["./lib/*"],
+ "@/components/*": ["./components/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],