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)
|
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
|
||||||
|
|
||||||
### 2.1 Système de design et composants UI
|
### 2.1 Système de design et composants UI
|
||||||
- [ ] Créer les composants UI de base (Button, Input, Card, Modal, etc.)
|
- [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge)
|
||||||
- [ ] Implémenter le système de design (couleurs, typographie, spacing)
|
- [x] Implémenter le système de design tech dark (couleurs, typographie, spacing)
|
||||||
- [ ] Setup Tailwind CSS avec design tokens personnalisés
|
- [x] Setup Tailwind CSS avec classes utilitaires personnalisées
|
||||||
- [ ] Créer une palette de couleurs moderne et accessible
|
- [x] Créer une palette de couleurs tech/cyberpunk
|
||||||
|
|
||||||
### 2.2 Composants Kanban existants (à améliorer)
|
### 2.2 Composants Kanban existants (à améliorer)
|
||||||
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
|
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
|
||||||
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
|
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
|
||||||
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
|
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
|
||||||
- [x] `components/ui/Header.tsx` - Header avec statistiques
|
- [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
|
### 2.3 Gestion des tâches (CRUD)
|
||||||
- [ ] `clients/tasks-client.ts` - Client pour les tâches
|
- [ ] 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
|
- [ ] `clients/base/http-client.ts` - Client HTTP de base
|
||||||
- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches
|
- [ ] `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
|
- [ ] `hooks/useKanban.ts` - Hook pour drag & drop
|
||||||
- [ ] Gestion des erreurs et loading states
|
- [ ] 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)
|
- [ ] Drag & drop entre colonnes (react-beautiful-dnd)
|
||||||
- [ ] Formulaires de création/édition de tâches
|
- [ ] Filtrage par statut/priorité/assigné
|
||||||
- [ ] Filtrage par tags/statut/priorité
|
|
||||||
- [ ] Recherche en temps réel dans les tâches
|
- [ ] 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)
|
## 📊 Phase 3: Dashboard et analytics (Priorité 3)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Task, TaskStatus } from '@/lib/types';
|
import { Task, TaskStatus } from '@/lib/types';
|
||||||
import { TaskCard } from './TaskCard';
|
import { TaskCard } from './TaskCard';
|
||||||
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
|
||||||
interface KanbanColumnProps {
|
interface KanbanColumnProps {
|
||||||
id: TaskStatus;
|
id: TaskStatus;
|
||||||
@@ -47,43 +49,45 @@ export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) {
|
|||||||
cancelled: '✕'
|
cancelled: '✕'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const badgeVariant = color === 'green' ? 'success' : color === 'blue' ? 'primary' : color === 'red' ? 'danger' : 'default';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 w-80 h-full">
|
<div className="flex-shrink-0 w-80 h-full">
|
||||||
{/* Header tech avec glow */}
|
<Card variant="elevated" className="h-full flex flex-col">
|
||||||
<div className={`bg-slate-900 ${style.border} border rounded-t-lg p-4 ${style.glow} shadow-lg`}>
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
||||||
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
|
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
|
||||||
<span className={`${style.badge} px-3 py-1 rounded-full text-xs font-mono font-bold`}>
|
|
||||||
{String(tasks.length).padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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`}>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{tasks.length === 0 ? (
|
|
||||||
<div className="text-center py-20">
|
|
||||||
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-slate-800 border-2 border-dashed ${style.border} flex items-center justify-center`}>
|
|
||||||
<span className={`text-2xl ${style.accent} opacity-50`}>{techIcons[id]}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs font-mono text-slate-500 uppercase tracking-wide">NO DATA</p>
|
|
||||||
<div className="mt-2 flex justify-center">
|
|
||||||
<div className={`w-8 h-0.5 ${style.accent.replace('text-', 'bg-')} opacity-30`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<Badge variant={badgeVariant} size="sm">
|
||||||
tasks.map((task) => (
|
{String(tasks.length).padStart(2, '0')}
|
||||||
<TaskCard key={task.id} task={task} />
|
</Badge>
|
||||||
))
|
</div>
|
||||||
)}
|
</CardHeader>
|
||||||
</div>
|
|
||||||
</div>
|
<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">
|
||||||
|
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-slate-800 border-2 border-dashed ${style.border} flex items-center justify-center`}>
|
||||||
|
<span className={`text-2xl ${style.accent} opacity-50`}>{techIcons[id]}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-mono text-slate-500 uppercase tracking-wide">NO DATA</p>
|
||||||
|
<div className="mt-2 flex justify-center">
|
||||||
|
<div className={`w-8 h-0.5 ${style.accent.replace('text-', 'bg-')} opacity-30`}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
tasks.map((task) => (
|
||||||
|
<TaskCard key={task.id} task={task} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Task } from '@/lib/types';
|
import { Task } from '@/lib/types';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { fr } from 'date-fns/locale';
|
import { fr } from 'date-fns/locale';
|
||||||
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -12,22 +14,9 @@ export function TaskCard({ task }: TaskCardProps) {
|
|||||||
const emojis = task.title.match(emojiRegex) || [];
|
const emojis = task.title.match(emojiRegex) || [];
|
||||||
const titleWithoutEmojis = task.title.replace(emojiRegex, '').trim();
|
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 (
|
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 */}
|
{/* Header tech avec titre et status */}
|
||||||
<div className="flex items-start gap-2 mb-2">
|
<div className="flex items-start gap-2 mb-2">
|
||||||
{emojis.length > 0 && (
|
{emojis.length > 0 && (
|
||||||
@@ -59,21 +48,23 @@ export function TaskCard({ task }: TaskCardProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags tech style */}
|
{/* Tags avec composant Badge */}
|
||||||
{task.tags && task.tags.length > 0 && (
|
{task.tags && task.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mb-3">
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
{task.tags.slice(0, 3).map((tag, index) => (
|
{task.tags.slice(0, 3).map((tag, index) => (
|
||||||
<span
|
<Badge
|
||||||
key={index}
|
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}
|
{tag}
|
||||||
</span>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{task.tags.length > 3 && (
|
{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}
|
+{task.tags.length - 3}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -98,6 +89,6 @@ export function TaskCard({ task }: TaskCardProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
interface HeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -69,49 +71,25 @@ interface StatCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ label, value, color }: StatCardProps) {
|
function StatCard({ label, value, color }: StatCardProps) {
|
||||||
const techStyles = {
|
|
||||||
blue: {
|
const textColors = {
|
||||||
bg: 'bg-slate-800/50',
|
blue: 'text-cyan-300',
|
||||||
border: 'border-cyan-500/30',
|
green: 'text-emerald-300',
|
||||||
text: 'text-cyan-300',
|
yellow: 'text-yellow-300',
|
||||||
glow: 'shadow-cyan-500/20'
|
gray: 'text-slate-300',
|
||||||
},
|
purple: 'text-purple-300'
|
||||||
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 style = techStyles[color];
|
|
||||||
|
|
||||||
return (
|
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`}>
|
<Card variant="elevated" className="px-3 py-2 hover:border-opacity-75 transition-all duration-300">
|
||||||
<div className={`text-xs font-mono font-bold ${style.text} opacity-75 uppercase tracking-wider`}>
|
<CardContent className="p-0">
|
||||||
{label}
|
<div className={`text-xs font-mono font-bold ${textColors[color]} opacity-75 uppercase tracking-wider`}>
|
||||||
</div>
|
{label}
|
||||||
<div className={`text-lg font-mono font-bold ${style.text}`}>
|
</div>
|
||||||
{value}
|
<div className={`text-lg font-mono font-bold ${textColors[color]}`}>
|
||||||
</div>
|
{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",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@@ -2717,6 +2719,15 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@@ -7409,6 +7420,16 @@
|
|||||||
"url": "https://opencollective.com/synckit"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.13",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
"@/services/*": ["./services/*"],
|
"@/services/*": ["./services/*"],
|
||||||
"@/lib/*": ["./lib/*"]
|
"@/lib/*": ["./lib/*"],
|
||||||
|
"@/components/*": ["./components/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user