From 52d8332f0cc4de497625b8c61fc622e1b9de9685 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 10 Oct 2025 08:22:44 +0200 Subject: [PATCH] refactor(TaskSelector): enhance task selection logic and integrate shared component - Replaced TaskSelector with TaskSelectorWithData to streamline task selection. - Updated TaskSelector to accept tasks as a prop, improving data handling. - Removed unnecessary API calls and loading states, simplifying the component's logic. - Added new sections to UIShowcaseClient for better component visibility. --- src/components/notes/MarkdownEditor.tsx | 4 +- .../shared/TaskSelectorWithData.tsx | 54 +++ .../ui-showcase/UIShowcaseClient.tsx | 6 + .../ui-showcase/sections/IconsSection.tsx | 299 ++++++++++++++ .../sections/TaskSelectorSection.tsx | 364 ++++++++++++++++++ .../ui-showcase/sections/ToastSection.tsx | 208 ++++++++++ src/components/ui-showcase/sections/index.ts | 3 + src/components/ui/DynamicIcon.tsx | 0 src/components/ui/TaskSelector.tsx | 70 +--- src/hooks/useTaskSelector.ts | 79 ++++ 10 files changed, 1030 insertions(+), 57 deletions(-) create mode 100644 src/components/shared/TaskSelectorWithData.tsx create mode 100644 src/components/ui-showcase/sections/IconsSection.tsx create mode 100644 src/components/ui-showcase/sections/TaskSelectorSection.tsx create mode 100644 src/components/ui-showcase/sections/ToastSection.tsx delete mode 100644 src/components/ui/DynamicIcon.tsx create mode 100644 src/hooks/useTaskSelector.ts diff --git a/src/components/notes/MarkdownEditor.tsx b/src/components/notes/MarkdownEditor.tsx index 7451e27..18cd9ac 100644 --- a/src/components/notes/MarkdownEditor.tsx +++ b/src/components/notes/MarkdownEditor.tsx @@ -8,7 +8,7 @@ import rehypeSanitize from 'rehype-sanitize'; import { Eye, EyeOff, Edit3, X, CheckSquare2 } from 'lucide-react'; import { TagInput } from '@/components/ui/TagInput'; import { TagDisplay } from '@/components/ui/TagDisplay'; -import { TaskSelector } from '@/components/ui/TaskSelector'; +import { TaskSelectorWithData } from '@/components/shared/TaskSelectorWithData'; import { Tag, Task } from '@/lib/types'; interface MarkdownEditorProps { @@ -366,7 +366,7 @@ export function MarkdownEditor({ Tâche: - void; + placeholder?: string; + className?: string; + excludePinnedTasks?: boolean; + maxHeight?: string; +} + +export function TaskSelectorWithData({ + selectedTaskId, + onTaskSelect, + placeholder = 'Sélectionner une tâche...', + className = '', + excludePinnedTasks = true, + maxHeight = 'max-h-60', +}: TaskSelectorWithDataProps) { + const { tasks, loading, loaded } = useTaskSelector({ + selectedTaskId, + excludePinnedTasks, + }); + + // Afficher un état de chargement si les tâches ne sont pas encore chargées + if (!loaded && loading) { + return ( +
+ + Chargement des tâches... + +
+ ); + } + + return ( + + ); +} diff --git a/src/components/ui-showcase/UIShowcaseClient.tsx b/src/components/ui-showcase/UIShowcaseClient.tsx index 79d227e..93daf01 100644 --- a/src/components/ui-showcase/UIShowcaseClient.tsx +++ b/src/components/ui-showcase/UIShowcaseClient.tsx @@ -12,6 +12,9 @@ import { FeedbackSection, DataDisplaySection, DropdownsSection, + IconsSection, + TaskSelectorSection, + ToastSection, } from './sections'; export function UIShowcaseClient() { @@ -41,6 +44,9 @@ export function UIShowcaseClient() { + + + diff --git a/src/components/ui-showcase/sections/IconsSection.tsx b/src/components/ui-showcase/sections/IconsSection.tsx new file mode 100644 index 0000000..f866423 --- /dev/null +++ b/src/components/ui-showcase/sections/IconsSection.tsx @@ -0,0 +1,299 @@ +'use client'; + +import { Emoji } from '@/components/ui/Emoji'; +import { + ChevronUp, + ChevronDown, + ChevronsUpDown, + Home, + User, + Settings, + Search, + Plus, + Star, + Heart, + Check, + AlertTriangle, + Info, +} from 'lucide-react'; + +export function IconsSection() { + const sampleIcons = [ + { name: 'home', component: Home }, + { name: 'user', component: User }, + { name: 'settings', component: Settings }, + { name: 'search', component: Search }, + { name: 'plus', component: Plus }, + { name: 'star', component: Star }, + { name: 'heart', component: Heart }, + { name: 'check', component: Check }, + { name: 'info', component: Info }, + { name: 'warning', component: AlertTriangle }, + ]; + + const sampleEmojis = [ + '🎯', + '✅', + '❌', + '⚠️', + '📋', + '📊', + '🔧', + '⚙️', + '🚀', + '💡', + '🔥', + '⭐', + '🎉', + '👥', + '📝', + '🔍', + '📅', + '⏰', + '💻', + '📱', + '🎨', + '🔒', + '🔓', + '📈', + '📉', + '💰', + '🏆', + '🎪', + '🌈', + '🌟', + ]; + + return ( +
+

+ Icons & Emojis +

+ +
+ {/* Lucide Icons */} +
+

+ Lucide Icons +

+
+
+

+ Icônes avec différentes tailles +

+
+ + + + + + +
+
+ +
+

+ Icônes avec différentes couleurs +

+
+ + + + + + +
+
+ +
+

+ Collection d'icônes disponibles +

+
+ {sampleIcons.map((icon) => ( +
+ + + {icon.name} + +
+ ))} +
+
+ +
+

+ États interactifs +

+
+ + + + +
+
+
+
+ + {/* Emojis */} +
+

+ Emojis +

+
+
+

+ Emojis avec différentes tailles +

+
+ + + + + +
+
+ +
+

+ Collection d'emojis disponibles +

+
+ {sampleEmojis.map((emoji) => ( +
+ +
+ ))} +
+
+ +
+

+ Utilisation dans différents contextes +

+
+
+ +
+
+ Tâche importante +
+
+ Due dans 2 jours +
+
+
+
+ +
+
+ Projet terminé +
+
+ Félicitations ! +
+
+
+
+ +
+
+ Attention requise +
+
+ Action nécessaire +
+
+
+
+
+
+
+ + {/* Sort Icons */} +
+

+ Sort Icons +

+
+
+

+ États de tri +

+
+
+ + + Aucun tri + +
+
+ + + Tri croissant + +
+
+ + + Tri décroissant + +
+
+
+ +
+

+ Utilisation dans les en-têtes de tableau +

+
+
+
+ + Nom + + +
+
+ + Date + + +
+
+ + Priorité + + +
+
+
+
+ Alice Johnson +
+
+ Bob Smith +
+
+ Charlie Brown +
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/ui-showcase/sections/TaskSelectorSection.tsx b/src/components/ui-showcase/sections/TaskSelectorSection.tsx new file mode 100644 index 0000000..cd3464d --- /dev/null +++ b/src/components/ui-showcase/sections/TaskSelectorSection.tsx @@ -0,0 +1,364 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Task } from '@/lib/types'; +import { Button } from '@/components/ui/Button'; +import { Search, X, CheckSquare2 } from 'lucide-react'; + +// Données mock pour le showcase +const mockTasks: Task[] = [ + { + id: '1', + title: 'Implement user authentication', + description: 'Add login and registration functionality', + priority: 'high', + status: 'in_progress', + dueDate: new Date(Date.now() + 3 * 86400000), + tags: ['auth', 'security'], + createdAt: new Date(), + updatedAt: new Date(), + source: 'manual', + sourceId: '1', + tagDetails: [], + }, + { + id: '2', + title: 'Design new dashboard', + description: 'Create a modern dashboard interface', + priority: 'medium', + status: 'todo', + dueDate: new Date(Date.now() + 7 * 86400000), + tags: ['design', 'ui'], + createdAt: new Date(), + updatedAt: new Date(), + source: 'manual', + sourceId: '2', + tagDetails: [], + }, + { + id: '3', + title: 'Fix critical bug in payment system', + description: 'Resolve issue with payment processing', + priority: 'high', + status: 'todo', + dueDate: new Date(Date.now() + 1 * 86400000), + tags: ['bug', 'payment'], + createdAt: new Date(), + updatedAt: new Date(), + source: 'manual', + sourceId: '3', + tagDetails: [], + }, + { + id: '4', + title: 'Write unit tests', + description: 'Add comprehensive test coverage', + priority: 'medium', + status: 'done', + dueDate: new Date(Date.now() - 2 * 86400000), + tags: ['testing', 'quality'], + createdAt: new Date(), + updatedAt: new Date(), + source: 'manual', + sourceId: '4', + tagDetails: [], + }, + { + id: '5', + title: 'Update documentation', + description: 'Refresh API documentation', + priority: 'low', + status: 'todo', + dueDate: new Date(Date.now() + 14 * 86400000), + tags: ['documentation'], + createdAt: new Date(), + updatedAt: new Date(), + source: 'manual', + sourceId: '5', + tagDetails: [], + }, +]; + +// Composant TaskSelector mock pour le showcase +interface MockTaskSelectorProps { + selectedTaskId?: string; + onTaskSelect: (task: Task | null) => void; + placeholder?: string; + className?: string; +} + +function MockTaskSelector({ + selectedTaskId, + onTaskSelect, + placeholder = 'Sélectionner une tâche...', + className = '', +}: MockTaskSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const [taskSearch, setTaskSearch] = useState(''); + const [selectedTask, setSelectedTask] = useState(undefined); + + // Trouver la tâche sélectionnée + useEffect(() => { + if (selectedTaskId) { + const task = mockTasks.find((t) => t.id === selectedTaskId); + setSelectedTask(task); + } else { + setSelectedTask(undefined); + } + }, [selectedTaskId]); + + // Filtrer les tâches selon la recherche + const filteredTasks = mockTasks.filter((task) => { + return ( + task.title.toLowerCase().includes(taskSearch.toLowerCase()) || + (task.description && + task.description.toLowerCase().includes(taskSearch.toLowerCase())) + ); + }); + + const handleTaskSelect = (task: Task) => { + console.log('Task selected:', task.title); // Debug + setSelectedTask(task); + onTaskSelect(task); + setIsOpen(false); + setTaskSearch(''); + }; + + const handleClearTask = () => { + setSelectedTask(undefined); + onTaskSelect(null); + }; + + return ( +
+ {/* Trigger Button */} +
{ + console.log('Dropdown clicked, isOpen:', isOpen); // Debug + setIsOpen(!isOpen); + }} + className="w-full flex items-center justify-between px-3 py-2 text-sm bg-[var(--card)] border border-[var(--border)] rounded-md hover:bg-[var(--card-hover)] transition-colors cursor-pointer" + > +
+ {selectedTask ? ( + <> + + + {selectedTask.title} + + + ) : ( + + {placeholder} + + )} +
+ {selectedTask && ( + + )} +
+ + {/* Dropdown Simple pour le showcase */} + {isOpen && ( +
+ {/* Search Input */} +
+
+ + setTaskSearch(e.target.value)} + className="w-full pl-9 pr-3 py-2 text-sm bg-[var(--input)] border border-[var(--border)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/20 focus:border-[var(--primary)]" + autoFocus + /> +
+
+ + {/* Tasks List */} +
+ {filteredTasks.length === 0 ? ( +
+ {taskSearch + ? 'Aucune tâche trouvée' + : 'Aucune tâche disponible'} +
+ ) : ( + filteredTasks.map((task) => ( + + )) + )} +
+
+ )} +
+ ); +} + +export function TaskSelectorSection() { + const [selectedTask, setSelectedTask] = useState(null); + + const handleTaskSelect = (task: Task | null) => { + setSelectedTask(task); + }; + + return ( +
+

+ Task Selector +

+ +
+ {/* Basic Task Selector */} +
+

+ Basic Task Selector +

+
+ + + {selectedTask && ( +
+

+ Tâche sélectionnée +

+
+ Titre: {selectedTask.title} +
+ {selectedTask.description && ( +
+ Description: {selectedTask.description} +
+ )} +
+ Priorité: {selectedTask.priority} +
+
+ Statut: {selectedTask.status} +
+
+ )} +
+
+ + {/* Task Selector States */} +
+

+ États du Task Selector +

+
+ {/* Avec tâche sélectionnée */} +
+

+ Avec sélection +

+ {}} + placeholder="Tâche pré-sélectionnée..." + className="max-w-sm" + /> +
+ + {/* Sans sélection */} +
+

+ Sans sélection +

+ {}} + placeholder="Aucune tâche sélectionnée..." + className="max-w-sm" + /> +
+
+
+ + {/* Utilisation dans un formulaire */} +
+

+ Utilisation dans un formulaire +

+
+
+
+ + +
+ +
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/components/ui-showcase/sections/ToastSection.tsx b/src/components/ui-showcase/sections/ToastSection.tsx new file mode 100644 index 0000000..8f95c67 --- /dev/null +++ b/src/components/ui-showcase/sections/ToastSection.tsx @@ -0,0 +1,208 @@ +'use client'; + +import { useState } from 'react'; +import { ToastProvider, useToast } from '@/components/ui/Toast'; +import { Button } from '@/components/ui/Button'; + +function ToastDemo() { + const { showToast } = useToast(); + + return ( +
+

+ Toast Notifications +

+ +
+ {/* Toast Types */} +
+

+ Types de Toast +

+
+ + + + +
+
+ + {/* Toast avec différentes durées */} +
+

+ Durées différentes +

+
+ + + +
+
+ + {/* Toast multiples */} +
+

+ Toast multiples +

+
+ + +
+
+ + {/* Toast dans différents contextes */} +
+

+ Contextes d'utilisation +

+
+
+

+ Actions utilisateur +

+
+ + + +
+
+ +
+

+ Notifications système +

+
+ + + +
+
+
+
+
+
+ ); +} + +export function ToastSection() { + return ( + + + + ); +} diff --git a/src/components/ui-showcase/sections/index.ts b/src/components/ui-showcase/sections/index.ts index b2995e6..f62691f 100644 --- a/src/components/ui-showcase/sections/index.ts +++ b/src/components/ui-showcase/sections/index.ts @@ -7,3 +7,6 @@ export { NavigationSection } from './NavigationSection'; export { FeedbackSection } from './FeedbackSection'; export { DataDisplaySection } from './DataDisplaySection'; export { DropdownsSection } from './DropdownsSection'; +export { IconsSection } from './IconsSection'; +export { TaskSelectorSection } from './TaskSelectorSection'; +export { ToastSection } from './ToastSection'; diff --git a/src/components/ui/DynamicIcon.tsx b/src/components/ui/DynamicIcon.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/ui/TaskSelector.tsx b/src/components/ui/TaskSelector.tsx index 4d9e251..fc36a62 100644 --- a/src/components/ui/TaskSelector.tsx +++ b/src/components/ui/TaskSelector.tsx @@ -3,30 +3,30 @@ import { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { Task } from '@/lib/types'; -import { tasksClient } from '@/clients/tasks-client'; import { Search, X, CheckSquare2 } from 'lucide-react'; interface TaskSelectorProps { + tasks: Task[]; selectedTaskId?: string; onTaskSelect: (task: Task | null) => void; placeholder?: string; className?: string; - excludePinnedTasks?: boolean; // Exclure les tâches avec des tags "objectif principal" - maxHeight?: string; // Hauteur maximale du dropdown + excludePinnedTasks?: boolean; + maxHeight?: string; + loading?: boolean; } export function TaskSelector({ + tasks, selectedTaskId, onTaskSelect, placeholder = 'Sélectionner une tâche...', className = '', excludePinnedTasks = true, maxHeight = 'max-h-60', + loading = false, }: TaskSelectorProps) { const [isOpen, setIsOpen] = useState(false); - const [allTasks, setAllTasks] = useState([]); - const [tasksLoading, setTasksLoading] = useState(false); - const [tasksLoaded, setTasksLoaded] = useState(false); // Nouvel état pour tracker le chargement const [taskSearch, setTaskSearch] = useState(''); const [selectedTask, setSelectedTask] = useState(undefined); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); @@ -35,23 +35,15 @@ export function TaskSelector({ const containerRef = useRef(null); const dropdownRef = useRef(null); - // Charger la tâche sélectionnée dès le montage si elle existe + // Trouver la tâche sélectionnée useEffect(() => { - if (selectedTaskId && !tasksLoaded) { - setTasksLoading(true); - tasksClient - .getTasks() - .then((response: { data: Task[] }) => { - setAllTasks(response.data); - setTasksLoaded(true); - // Trouver la tâche sélectionnée - const task = response.data.find((t: Task) => t.id === selectedTaskId); - setSelectedTask(task); - }) - .catch(console.error) - .finally(() => setTasksLoading(false)); + if (selectedTaskId) { + const task = tasks.find((t) => t.id === selectedTaskId); + setSelectedTask(task); + } else { + setSelectedTask(undefined); } - }, [selectedTaskId, tasksLoaded]); + }, [selectedTaskId, tasks]); // Calculer la position du dropdown useEffect(() => { @@ -67,40 +59,8 @@ export function TaskSelector({ } }, [isOpen]); - // Charger toutes les tâches quand le dropdown s'ouvre (si pas déjà chargées) - useEffect(() => { - if (isOpen && !tasksLoaded) { - setTasksLoading(true); - tasksClient - .getTasks() - .then((response: { data: Task[] }) => { - setAllTasks(response.data); - setTasksLoaded(true); - // Trouver la tâche sélectionnée si elle existe - if (selectedTaskId) { - const task = response.data.find( - (t: Task) => t.id === selectedTaskId - ); - setSelectedTask(task); - } - }) - .catch(console.error) - .finally(() => setTasksLoading(false)); - } - }, [isOpen, selectedTaskId, tasksLoaded]); - - // Mettre à jour la tâche sélectionnée quand selectedTaskId change - useEffect(() => { - if (selectedTaskId && tasksLoaded) { - const task = allTasks.find((t: Task) => t.id === selectedTaskId); - setSelectedTask(task); - } else if (!selectedTaskId) { - setSelectedTask(undefined); - } - }, [selectedTaskId, tasksLoaded, allTasks]); - // Filtrer les tâches selon la recherche et les options - const filteredTasks = allTasks.filter((task) => { + const filteredTasks = tasks.filter((task) => { // Exclure les tâches avec des tags marqués comme "objectif principal" si demandé if ( excludePinnedTasks && @@ -195,7 +155,7 @@ export function TaskSelector({ {/* Tasks List */}
- {tasksLoading ? ( + {loading ? (
Chargement...
diff --git a/src/hooks/useTaskSelector.ts b/src/hooks/useTaskSelector.ts new file mode 100644 index 0000000..476bfc1 --- /dev/null +++ b/src/hooks/useTaskSelector.ts @@ -0,0 +1,79 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Task } from '@/lib/types'; +import { tasksClient } from '@/clients/tasks-client'; + +interface UseTaskSelectorProps { + selectedTaskId?: string; + excludePinnedTasks?: boolean; +} + +export function useTaskSelector({ + selectedTaskId, + excludePinnedTasks = true, +}: UseTaskSelectorProps = {}) { + const [allTasks, setAllTasks] = useState([]); + const [tasksLoading, setTasksLoading] = useState(false); + const [tasksLoaded, setTasksLoaded] = useState(false); + const [selectedTask, setSelectedTask] = useState(undefined); + + // Charger toutes les tâches + const loadTasks = useCallback(async () => { + if (tasksLoaded) return; + + setTasksLoading(true); + try { + const response = await tasksClient.getTasks(); + setAllTasks(response.data); + setTasksLoaded(true); + } catch (error) { + console.error('Error loading tasks:', error); + } finally { + setTasksLoading(false); + } + }, [tasksLoaded]); + + // Charger les tâches au montage + useEffect(() => { + loadTasks(); + }, [loadTasks]); + + // Mettre à jour la tâche sélectionnée quand selectedTaskId change + useEffect(() => { + if (selectedTaskId && tasksLoaded) { + const task = allTasks.find((t: Task) => t.id === selectedTaskId); + setSelectedTask(task); + } else if (!selectedTaskId) { + setSelectedTask(undefined); + } + }, [selectedTaskId, tasksLoaded, allTasks]); + + // Filtrer les tâches selon les options + const filteredTasks = allTasks.filter((task) => { + // Exclure les tâches avec des tags marqués comme "objectif principal" si demandé + if ( + excludePinnedTasks && + task.tagDetails && + task.tagDetails.some((tag) => tag.isPinned) + ) { + return false; + } + return true; + }); + + // Fonction pour forcer le rechargement + const refreshTasks = useCallback(async () => { + setTasksLoaded(false); + setAllTasks([]); + await loadTasks(); + }, [loadTasks]); + + return { + tasks: filteredTasks, + selectedTask, + loading: tasksLoading, + loaded: tasksLoaded, + refreshTasks, + }; +}