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.
This commit is contained in:
Julien Froidefond
2025-10-10 08:22:44 +02:00
parent 7811453e02
commit 52d8332f0c
10 changed files with 1030 additions and 57 deletions

View File

@@ -8,7 +8,7 @@ import rehypeSanitize from 'rehype-sanitize';
import { Eye, EyeOff, Edit3, X, CheckSquare2 } from 'lucide-react'; import { Eye, EyeOff, Edit3, X, CheckSquare2 } from 'lucide-react';
import { TagInput } from '@/components/ui/TagInput'; import { TagInput } from '@/components/ui/TagInput';
import { TagDisplay } from '@/components/ui/TagDisplay'; import { TagDisplay } from '@/components/ui/TagDisplay';
import { TaskSelector } from '@/components/ui/TaskSelector'; import { TaskSelectorWithData } from '@/components/shared/TaskSelectorWithData';
import { Tag, Task } from '@/lib/types'; import { Tag, Task } from '@/lib/types';
interface MarkdownEditorProps { interface MarkdownEditorProps {
@@ -366,7 +366,7 @@ export function MarkdownEditor({
<span className="text-sm font-medium text-[var(--foreground)]"> <span className="text-sm font-medium text-[var(--foreground)]">
Tâche: Tâche:
</span> </span>
<TaskSelector <TaskSelectorWithData
selectedTaskId={selectedTaskId} selectedTaskId={selectedTaskId}
onTaskSelect={onTaskChange} onTaskSelect={onTaskChange}
placeholder="Associer à une tâche..." placeholder="Associer à une tâche..."

View File

@@ -0,0 +1,54 @@
'use client';
import { Task } from '@/lib/types';
import { TaskSelector } from '@/components/ui/TaskSelector';
import { useTaskSelector } from '@/hooks/useTaskSelector';
interface TaskSelectorWithDataProps {
selectedTaskId?: string;
onTaskSelect: (task: Task | null) => 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 (
<div
className={`${className} flex items-center justify-center px-3 py-2 text-sm bg-[var(--card)] border border-[var(--border)] rounded-md`}
>
<span className="text-[var(--muted-foreground)]">
Chargement des tâches...
</span>
</div>
);
}
return (
<TaskSelector
tasks={tasks}
selectedTaskId={selectedTaskId}
onTaskSelect={onTaskSelect}
placeholder={placeholder}
className={className}
excludePinnedTasks={excludePinnedTasks}
maxHeight={maxHeight}
loading={loading}
/>
);
}

View File

@@ -12,6 +12,9 @@ import {
FeedbackSection, FeedbackSection,
DataDisplaySection, DataDisplaySection,
DropdownsSection, DropdownsSection,
IconsSection,
TaskSelectorSection,
ToastSection,
} from './sections'; } from './sections';
export function UIShowcaseClient() { export function UIShowcaseClient() {
@@ -41,6 +44,9 @@ export function UIShowcaseClient() {
<NavigationSection /> <NavigationSection />
<FeedbackSection /> <FeedbackSection />
<DataDisplaySection /> <DataDisplaySection />
<IconsSection />
<TaskSelectorSection />
<ToastSection />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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 (
<section id="icons" className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Icons & Emojis
</h2>
<div className="space-y-8">
{/* Lucide Icons */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Lucide Icons
</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Icônes avec différentes tailles
</p>
<div className="flex flex-wrap items-center gap-4">
<Home size={16} />
<User size={20} />
<Settings size={24} />
<Search size={28} />
<Plus size={32} />
<Star size={36} />
</div>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Icônes avec différentes couleurs
</p>
<div className="flex flex-wrap items-center gap-4">
<Heart className="text-[var(--destructive)]" />
<Star className="text-[var(--accent)]" />
<Check className="text-[var(--success)]" />
<AlertTriangle className="text-[var(--yellow)]" />
<Info className="text-[var(--primary)]" />
<Settings className="text-[var(--purple)]" />
</div>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Collection d'icônes disponibles
</p>
<div className="grid grid-cols-5 gap-3 p-4 bg-[var(--card)] rounded-lg border border-[var(--border)]">
{sampleIcons.map((icon) => (
<div
key={icon.name}
className="flex flex-col items-center space-y-1"
>
<icon.component size={20} />
<span className="text-xs text-[var(--muted-foreground)] text-center">
{icon.name}
</span>
</div>
))}
</div>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
États interactifs
</p>
<div className="flex flex-wrap items-center gap-4">
<button className="p-2 hover:bg-[var(--card-hover)] rounded-md transition-colors">
<Heart className="text-[var(--muted-foreground)] hover:text-[var(--destructive)] transition-colors" />
</button>
<button className="p-2 hover:bg-[var(--card-hover)] rounded-md transition-colors">
<Star className="text-[var(--muted-foreground)] hover:text-[var(--accent)] transition-colors" />
</button>
<button className="p-2 hover:bg-[var(--card-hover)] rounded-md transition-colors">
<Settings className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors" />
</button>
<button className="p-2 hover:bg-[var(--card-hover)] rounded-md transition-colors">
<Check className="text-[var(--muted-foreground)] hover:text-[var(--success)] transition-colors" />
</button>
</div>
</div>
</div>
</div>
{/* Emojis */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Emojis
</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Emojis avec différentes tailles
</p>
<div className="flex flex-wrap items-center gap-4">
<Emoji emoji="🎯" className="text-sm" />
<Emoji emoji="✅" className="text-base" />
<Emoji emoji="🚀" className="text-lg" />
<Emoji emoji="💡" className="text-xl" />
<Emoji emoji="🔥" className="text-2xl" />
</div>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Collection d'emojis disponibles
</p>
<div className="grid grid-cols-10 gap-3 p-4 bg-[var(--card)] rounded-lg border border-[var(--border)]">
{sampleEmojis.map((emoji) => (
<div key={emoji} className="flex justify-center">
<Emoji emoji={emoji} className="text-lg" />
</div>
))}
</div>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Utilisation dans différents contextes
</p>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<Emoji emoji="📋" className="text-lg" />
<div>
<div className="font-medium text-[var(--foreground)]">
Tâche importante
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Due dans 2 jours
</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<Emoji emoji="🎉" className="text-lg" />
<div>
<div className="font-medium text-[var(--foreground)]">
Projet terminé
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Félicitations !
</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<Emoji emoji="⚠️" className="text-lg" />
<div>
<div className="font-medium text-[var(--foreground)]">
Attention requise
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Action nécessaire
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Sort Icons */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Sort Icons
</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
États de tri
</p>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<ChevronsUpDown size={16} />
<span className="text-sm text-[var(--muted-foreground)]">
Aucun tri
</span>
</div>
<div className="flex items-center gap-2">
<ChevronUp size={16} />
<span className="text-sm text-[var(--muted-foreground)]">
Tri croissant
</span>
</div>
<div className="flex items-center gap-2">
<ChevronDown size={16} />
<span className="text-sm text-[var(--muted-foreground)]">
Tri décroissant
</span>
</div>
</div>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">
Utilisation dans les en-têtes de tableau
</p>
<div className="bg-[var(--card)] rounded-lg border border-[var(--border)] overflow-hidden">
<div className="grid grid-cols-3 gap-4 p-4 bg-[var(--card-column)] border-b border-[var(--border)]">
<div className="flex items-center gap-2 cursor-pointer hover:bg-[var(--card-hover)] p-2 rounded-md transition-colors">
<span className="text-sm font-medium text-[var(--foreground)]">
Nom
</span>
<ChevronUp size={14} />
</div>
<div className="flex items-center gap-2 cursor-pointer hover:bg-[var(--card-hover)] p-2 rounded-md transition-colors">
<span className="text-sm font-medium text-[var(--foreground)]">
Date
</span>
<ChevronDown size={14} />
</div>
<div className="flex items-center gap-2 cursor-pointer hover:bg-[var(--card-hover)] p-2 rounded-md transition-colors">
<span className="text-sm font-medium text-[var(--foreground)]">
Priorité
</span>
<ChevronsUpDown size={14} />
</div>
</div>
<div className="p-4 space-y-2">
<div className="text-sm text-[var(--foreground)]">
Alice Johnson
</div>
<div className="text-sm text-[var(--foreground)]">
Bob Smith
</div>
<div className="text-sm text-[var(--foreground)]">
Charlie Brown
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -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<Task | undefined>(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 (
<div className={`relative ${className}`}>
{/* Trigger Button */}
<div
onClick={() => {
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"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
{selectedTask ? (
<>
<CheckSquare2 className="w-4 h-4 text-[var(--primary)] flex-shrink-0" />
<span className="truncate text-[var(--foreground)]">
{selectedTask.title}
</span>
</>
) : (
<span className="text-[var(--muted-foreground)]">
{placeholder}
</span>
)}
</div>
{selectedTask && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleClearTask();
}}
className="ml-2 p-1 hover:bg-[var(--destructive)]/10 rounded"
>
<X className="w-3 h-3 text-[var(--muted-foreground)]" />
</button>
)}
</div>
{/* Dropdown Simple pour le showcase */}
{isOpen && (
<div className="absolute top-full left-0 right-0 mt-1 bg-[var(--card)] border border-[var(--border)] rounded-md shadow-lg max-h-60 overflow-hidden z-50">
{/* Search Input */}
<div className="p-2 border-b border-[var(--border)]">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--muted-foreground)]" />
<input
type="text"
placeholder="Rechercher une tâche..."
value={taskSearch}
onChange={(e) => 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
/>
</div>
</div>
{/* Tasks List */}
<div className="max-h-48 overflow-y-auto">
{filteredTasks.length === 0 ? (
<div className="p-3 text-center text-[var(--muted-foreground)]">
{taskSearch
? 'Aucune tâche trouvée'
: 'Aucune tâche disponible'}
</div>
) : (
filteredTasks.map((task) => (
<button
key={task.id}
type="button"
onClick={() => handleTaskSelect(task)}
className="w-full px-3 py-2 text-left hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2"
>
<CheckSquare2 className="w-4 h-4 text-[var(--primary)] flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-[var(--foreground)] truncate">
{task.title}
</div>
{task.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate">
{task.description}
</div>
)}
</div>
{task.tags && task.tags.length > 0 && (
<div className="flex gap-1 flex-shrink-0">
{task.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="px-1.5 py-0.5 text-xs rounded bg-[var(--primary)]/20 text-[var(--primary)]"
>
{tag}
</span>
))}
</div>
)}
</button>
))
)}
</div>
</div>
)}
</div>
);
}
export function TaskSelectorSection() {
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const handleTaskSelect = (task: Task | null) => {
setSelectedTask(task);
};
return (
<section id="task-selector" className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Task Selector
</h2>
<div className="space-y-8">
{/* Basic Task Selector */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Basic Task Selector
</h3>
<div className="space-y-4">
<MockTaskSelector
selectedTaskId={selectedTask?.id}
onTaskSelect={handleTaskSelect}
placeholder="Sélectionner une tâche..."
className="max-w-md"
/>
{selectedTask && (
<div className="bg-[var(--card)] rounded-lg border border-[var(--border)] p-4">
<h4 className="font-medium text-[var(--foreground)] mb-2">
Tâche sélectionnée
</h4>
<div className="text-sm text-[var(--foreground)]">
<strong>Titre:</strong> {selectedTask.title}
</div>
{selectedTask.description && (
<div className="text-sm text-[var(--muted-foreground)] mt-1">
<strong>Description:</strong> {selectedTask.description}
</div>
)}
<div className="text-sm text-[var(--muted-foreground)] mt-1">
<strong>Priorité:</strong> {selectedTask.priority}
</div>
<div className="text-sm text-[var(--muted-foreground)] mt-1">
<strong>Statut:</strong> {selectedTask.status}
</div>
</div>
)}
</div>
</div>
{/* Task Selector States */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
États du Task Selector
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Avec tâche sélectionnée */}
<div className="space-y-3">
<h4 className="text-md font-medium text-[var(--foreground)]">
Avec sélection
</h4>
<MockTaskSelector
selectedTaskId="1"
onTaskSelect={() => {}}
placeholder="Tâche pré-sélectionnée..."
className="max-w-sm"
/>
</div>
{/* Sans sélection */}
<div className="space-y-3">
<h4 className="text-md font-medium text-[var(--foreground)]">
Sans sélection
</h4>
<MockTaskSelector
selectedTaskId={undefined}
onTaskSelect={() => {}}
placeholder="Aucune tâche sélectionnée..."
className="max-w-sm"
/>
</div>
</div>
</div>
{/* Utilisation dans un formulaire */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Utilisation dans un formulaire
</h3>
<div className="bg-[var(--card)] rounded-lg border border-[var(--border)] p-6">
<form className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Tâche à assigner
</label>
<MockTaskSelector
selectedTaskId={selectedTask?.id}
onTaskSelect={handleTaskSelect}
placeholder="Sélectionner une tâche à assigner..."
className="max-w-md"
/>
</div>
<div className="flex gap-2">
<Button
type="submit"
variant="primary"
disabled={!selectedTask}
>
Assigner{' '}
{selectedTask ? `"${selectedTask.title}"` : 'une tâche'}
</Button>
<Button
type="button"
variant="secondary"
onClick={() => setSelectedTask(null)}
>
Effacer la sélection
</Button>
</div>
</form>
</div>
</div>
</div>
</section>
);
}

View File

@@ -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 (
<section id="toast" className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Toast Notifications
</h2>
<div className="space-y-8">
{/* Toast Types */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Types de Toast
</h3>
<div className="flex flex-wrap gap-4">
<Button
onClick={() =>
showToast(
'Succès ! Votre action a été effectuée avec succès.',
5000,
'✅'
)
}
>
Toast Success
</Button>
<Button
onClick={() =>
showToast(
"Erreur: Une erreur est survenue lors de l'opération.",
7000,
'❌'
)
}
>
Toast Error
</Button>
<Button
onClick={() =>
showToast(
'Attention: Veuillez vérifier vos informations.',
6000,
'⚠️'
)
}
>
Toast Warning
</Button>
<Button
onClick={() =>
showToast(
'Information: Voici une information importante.',
5000,
''
)
}
>
Toast Info
</Button>
</div>
</div>
{/* Toast avec différentes durées */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Durées différentes
</h3>
<div className="flex flex-wrap gap-4">
<Button
onClick={() =>
showToast('Toast court - Disparaît rapidement', 2000, '⚡')
}
>
Durée courte (2s)
</Button>
<Button
onClick={() =>
showToast('Toast normal - Durée par défaut', 5000, '📝')
}
>
Durée normale (5s)
</Button>
<Button
onClick={() =>
showToast('Toast long - Disparaît lentement', 10000, '⏰')
}
>
Durée longue (10s)
</Button>
</div>
</div>
{/* Toast multiples */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Toast multiples
</h3>
<div className="flex flex-wrap gap-4">
<Button
onClick={() => {
showToast('Tâche 1 terminée', 5000, '✅');
showToast('Tâche 2 terminée', 5000, '✅');
showToast('Tâche 3 terminée', 5000, '✅');
}}
>
Ajouter 3 toasts
</Button>
<Button
onClick={() => {
showToast('Erreur critique', 5000, '❌');
showToast('Attention requise', 5000, '⚠️');
showToast('Information importante', 5000, '');
showToast('Opération réussie', 5000, '✅');
}}
>
Ajouter 4 toasts différents
</Button>
</div>
</div>
{/* Toast dans différents contextes */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">
Contextes d'utilisation
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<h4 className="text-md font-medium text-[var(--foreground)]">
Actions utilisateur
</h4>
<div className="space-y-2">
<Button
size="sm"
onClick={() =>
showToast('Tâche créée avec succès', 3000, '')
}
>
Créer une tâche
</Button>
<Button
size="sm"
onClick={() =>
showToast('Tâche supprimée définitivement', 3000, '🗑')
}
>
Supprimer une tâche
</Button>
<Button
size="sm"
onClick={() =>
showToast('Synchronisation en cours...', 3000, '🔄')
}
>
Synchroniser
</Button>
</div>
</div>
<div className="space-y-3">
<h4 className="text-md font-medium text-[var(--foreground)]">
Notifications système
</h4>
<div className="space-y-2">
<Button
size="sm"
onClick={() =>
showToast('Connexion faible détectée', 5000, '📶')
}
>
Connexion faible
</Button>
<Button
size="sm"
onClick={() => showToast('Erreur de sauvegarde', 5000, '💾')}
>
Erreur de sauvegarde
</Button>
<Button
size="sm"
onClick={() =>
showToast('Mise à jour disponible', 5000, '🔄')
}
>
Mise à jour
</Button>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
export function ToastSection() {
return (
<ToastProvider>
<ToastDemo />
</ToastProvider>
);
}

View File

@@ -7,3 +7,6 @@ export { NavigationSection } from './NavigationSection';
export { FeedbackSection } from './FeedbackSection'; export { FeedbackSection } from './FeedbackSection';
export { DataDisplaySection } from './DataDisplaySection'; export { DataDisplaySection } from './DataDisplaySection';
export { DropdownsSection } from './DropdownsSection'; export { DropdownsSection } from './DropdownsSection';
export { IconsSection } from './IconsSection';
export { TaskSelectorSection } from './TaskSelectorSection';
export { ToastSection } from './ToastSection';

View File

@@ -3,30 +3,30 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Task } from '@/lib/types'; import { Task } from '@/lib/types';
import { tasksClient } from '@/clients/tasks-client';
import { Search, X, CheckSquare2 } from 'lucide-react'; import { Search, X, CheckSquare2 } from 'lucide-react';
interface TaskSelectorProps { interface TaskSelectorProps {
tasks: Task[];
selectedTaskId?: string; selectedTaskId?: string;
onTaskSelect: (task: Task | null) => void; onTaskSelect: (task: Task | null) => void;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
excludePinnedTasks?: boolean; // Exclure les tâches avec des tags "objectif principal" excludePinnedTasks?: boolean;
maxHeight?: string; // Hauteur maximale du dropdown maxHeight?: string;
loading?: boolean;
} }
export function TaskSelector({ export function TaskSelector({
tasks,
selectedTaskId, selectedTaskId,
onTaskSelect, onTaskSelect,
placeholder = 'Sélectionner une tâche...', placeholder = 'Sélectionner une tâche...',
className = '', className = '',
excludePinnedTasks = true, excludePinnedTasks = true,
maxHeight = 'max-h-60', maxHeight = 'max-h-60',
loading = false,
}: TaskSelectorProps) { }: TaskSelectorProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [allTasks, setAllTasks] = useState<Task[]>([]);
const [tasksLoading, setTasksLoading] = useState(false);
const [tasksLoaded, setTasksLoaded] = useState(false); // Nouvel état pour tracker le chargement
const [taskSearch, setTaskSearch] = useState(''); const [taskSearch, setTaskSearch] = useState('');
const [selectedTask, setSelectedTask] = useState<Task | undefined>(undefined); const [selectedTask, setSelectedTask] = useState<Task | undefined>(undefined);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
@@ -35,23 +35,15 @@ export function TaskSelector({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
// Charger la tâche sélectionnée dès le montage si elle existe
useEffect(() => {
if (selectedTaskId && !tasksLoaded) {
setTasksLoading(true);
tasksClient
.getTasks()
.then((response: { data: Task[] }) => {
setAllTasks(response.data);
setTasksLoaded(true);
// Trouver la tâche sélectionnée // Trouver la tâche sélectionnée
const task = response.data.find((t: Task) => t.id === selectedTaskId); useEffect(() => {
if (selectedTaskId) {
const task = tasks.find((t) => t.id === selectedTaskId);
setSelectedTask(task); setSelectedTask(task);
}) } else {
.catch(console.error) setSelectedTask(undefined);
.finally(() => setTasksLoading(false));
} }
}, [selectedTaskId, tasksLoaded]); }, [selectedTaskId, tasks]);
// Calculer la position du dropdown // Calculer la position du dropdown
useEffect(() => { useEffect(() => {
@@ -67,40 +59,8 @@ export function TaskSelector({
} }
}, [isOpen]); }, [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 // 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é // Exclure les tâches avec des tags marqués comme "objectif principal" si demandé
if ( if (
excludePinnedTasks && excludePinnedTasks &&
@@ -195,7 +155,7 @@ export function TaskSelector({
{/* Tasks List */} {/* Tasks List */}
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
{tasksLoading ? ( {loading ? (
<div className="p-3 text-center text-[var(--muted-foreground)]"> <div className="p-3 text-center text-[var(--muted-foreground)]">
Chargement... Chargement...
</div> </div>

View File

@@ -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<Task[]>([]);
const [tasksLoading, setTasksLoading] = useState(false);
const [tasksLoaded, setTasksLoaded] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | undefined>(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,
};
}