diff --git a/TODO.md b/TODO.md index 91490cf..19ea599 100644 --- a/TODO.md +++ b/TODO.md @@ -33,10 +33,10 @@ ## Phase 2: Interface utilisateur Kanban (Priorité 2) ### 2.1 Composants de base -- [ ] `components/kanban/Board.tsx` - Tableau Kanban principal -- [ ] `components/kanban/Column.tsx` - Colonnes du Kanban -- [ ] `components/kanban/TaskCard.tsx` - Cartes de tâches -- [ ] `components/ui/` - Composants UI réutilisables +- [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 ### 2.2 Clients HTTP - [ ] `clients/tasks-client.ts` - Client pour les tâches diff --git a/components/kanban/Board.tsx b/components/kanban/Board.tsx new file mode 100644 index 0000000..36f046e --- /dev/null +++ b/components/kanban/Board.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { Task, TaskStatus } from '@/lib/types'; +import { KanbanColumn } from './Column'; +import { useMemo } from 'react'; + +interface KanbanBoardProps { + tasks: Task[]; +} + +export function KanbanBoard({ tasks }: KanbanBoardProps) { + // Organiser les tâches par statut + const tasksByStatus = useMemo(() => { + const grouped = tasks.reduce((acc, task) => { + if (!acc[task.status]) { + acc[task.status] = []; + } + acc[task.status].push(task); + return acc; + }, {} as Record); + + return grouped; + }, [tasks]); + + // Configuration des colonnes + const columns: Array<{ + id: TaskStatus; + title: string; + color: string; + tasks: Task[]; + }> = [ + { + id: 'todo', + title: 'À faire', + color: 'gray', + tasks: tasksByStatus.todo || [] + }, + { + id: 'in_progress', + title: 'En cours', + color: 'blue', + tasks: tasksByStatus.in_progress || [] + }, + { + id: 'done', + title: 'Terminé', + color: 'green', + tasks: tasksByStatus.done || [] + }, + { + id: 'cancelled', + title: 'Annulé', + color: 'red', + tasks: tasksByStatus.cancelled || [] + } + ]; + + return ( +
+ {columns.map((column) => ( + + ))} +
+ ); +} diff --git a/components/kanban/Column.tsx b/components/kanban/Column.tsx new file mode 100644 index 0000000..fe01c9e --- /dev/null +++ b/components/kanban/Column.tsx @@ -0,0 +1,57 @@ +import { Task, TaskStatus } from '@/lib/types'; +import { TaskCard } from './TaskCard'; + +interface KanbanColumnProps { + id: TaskStatus; + title: string; + color: string; + tasks: Task[]; +} + +export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) { + const colorClasses = { + gray: 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800', + blue: 'border-blue-300 bg-blue-50 dark:border-blue-600 dark:bg-blue-900/20', + green: 'border-green-300 bg-green-50 dark:border-green-600 dark:bg-green-900/20', + red: 'border-red-300 bg-red-50 dark:border-red-600 dark:bg-red-900/20' + }; + + const headerColorClasses = { + gray: 'text-gray-700 dark:text-gray-300', + blue: 'text-blue-700 dark:text-blue-300', + green: 'text-green-700 dark:text-green-300', + red: 'text-red-700 dark:text-red-300' + }; + + return ( +
+ {/* En-tête de colonne */} +
+
+

+ {title} +

+ + {tasks.length} + +
+
+ + {/* Zone de contenu */} +
+
+ {tasks.length === 0 ? ( +
+
📝
+

Aucune tâche

+
+ ) : ( + tasks.map((task) => ( + + )) + )} +
+
+
+ ); +} diff --git a/components/kanban/TaskCard.tsx b/components/kanban/TaskCard.tsx new file mode 100644 index 0000000..e682176 --- /dev/null +++ b/components/kanban/TaskCard.tsx @@ -0,0 +1,112 @@ +import { Task } from '@/lib/types'; +import { formatDistanceToNow } from 'date-fns'; +import { fr } from 'date-fns/locale'; + +interface TaskCardProps { + task: Task; +} + +export function TaskCard({ task }: TaskCardProps) { + // Extraire les emojis du titre pour les afficher comme tags visuels + const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu; + 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 ( +
+ {/* En-tête avec emojis */} + {emojis.length > 0 && ( +
+ {emojis.map((emoji, index) => ( + + {emoji} + + ))} +
+ )} + + {/* Titre */} +

+ {titleWithoutEmojis} +

+ + {/* Description si présente */} + {task.description && ( +

+ {task.description} +

+ )} + + {/* Tags */} +
+ {/* Priorité */} + + {task.priority} + + + {/* Source */} + + {task.source} + + + {/* Tags personnalisés */} + {task.tags && task.tags.length > 0 && ( + task.tags.slice(0, 2).map((tag, index) => ( + + #{tag} + + )) + )} +
+ + {/* Footer avec dates */} +
+
+ {task.dueDate && ( + + 📅 {formatDistanceToNow(new Date(task.dueDate), { + addSuffix: true, + locale: fr + })} + + )} +
+ +
+ {task.completedAt ? ( + + ✅ {formatDistanceToNow(new Date(task.completedAt), { + addSuffix: true, + locale: fr + })} + + ) : ( + + Créé {formatDistanceToNow(new Date(task.createdAt), { + addSuffix: true, + locale: fr + })} + + )} +
+
+
+ ); +} diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx new file mode 100644 index 0000000..156de42 --- /dev/null +++ b/components/ui/Header.tsx @@ -0,0 +1,87 @@ +interface HeaderProps { + title: string; + subtitle: string; + stats: { + total: number; + completed: number; + inProgress: number; + todo: number; + completionRate: number; + }; +} + +export function Header({ title, subtitle, stats }: HeaderProps) { + return ( +
+
+
+ {/* Titre et sous-titre */} +
+

+ {title} +

+

+ {subtitle} +

+
+ + {/* Statistiques */} +
+ + + + + +
+
+
+
+ ); +} + +interface StatCardProps { + label: string; + value: number | string; + color: 'blue' | 'green' | 'yellow' | 'gray' | 'purple'; +} + +function StatCard({ label, value, color }: StatCardProps) { + const colorClasses = { + blue: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800', + green: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800', + yellow: 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-300 dark:border-yellow-800', + gray: 'bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700', + purple: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800' + }; + + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 099d00e..dd8cbdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@prisma/client": "^6.16.1", + "date-fns": "^4.1.0", "next": "15.5.3", "prisma": "^6.16.1", "react": "19.1.0", @@ -2883,6 +2884,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", diff --git a/package.json b/package.json index 7125f3b..8fe4e74 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@prisma/client": "^6.16.1", + "date-fns": "^4.1.0", "next": "15.5.3", "prisma": "^6.16.1", "react": "19.1.0", diff --git a/src/app/page.tsx b/src/app/page.tsx index a932894..c6dd65b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,28 @@ -import Image from "next/image"; +import { taskProcessorService } from '@/services/task-processor'; +import { getTargetRemindersList } from '@/lib/config'; +import { KanbanBoard } from '../../components/kanban/Board'; +import { Header } from '../../components/ui/Header'; + +export default async function HomePage() { + // SSR - Récupération des données côté serveur + const [tasks, stats] = await Promise.all([ + taskProcessorService.getTasks({ limit: 100 }), + taskProcessorService.getTaskStats() + ]); + + const targetList = getTargetRemindersList(); -export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
+
+
+ +
+
-
); }