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

38
TODO.md
View File

@@ -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)

View File

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

View File

@@ -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
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 { 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
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
);
}

9
lib/utils.ts Normal file
View 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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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"],