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:
Julien Froidefond
2025-09-14 08:23:04 +02:00
parent 124e8baee8
commit 79f8035d18
13 changed files with 421 additions and 110 deletions

View File

@@ -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 (
<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`}>
<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>
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
{title}
</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>
<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>
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
{title}
</h3>
</div>
) : (
tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))
)}
</div>
</div>
<Badge variant={badgeVariant} size="sm">
{String(tasks.length).padStart(2, '0')}
</Badge>
</div>
</CardHeader>
<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>
);
}

View File

@@ -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
View 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
View 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
View 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 };

View File

@@ -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`}>
{label}
</div>
<div className={`text-lg font-mono font-bold ${style.text}`}>
{value}
</div>
</div>
<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 ${textColors[color]}`}>
{value}
</div>
</CardContent>
</Card>
);
}

44
components/ui/Input.tsx Normal file
View 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
View 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
);
}