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:
@@ -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..."
|
||||||
|
|||||||
54
src/components/shared/TaskSelectorWithData.tsx
Normal file
54
src/components/shared/TaskSelectorWithData.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
299
src/components/ui-showcase/sections/IconsSection.tsx
Normal file
299
src/components/ui-showcase/sections/IconsSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
364
src/components/ui-showcase/sections/TaskSelectorSection.tsx
Normal file
364
src/components/ui-showcase/sections/TaskSelectorSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
208
src/components/ui-showcase/sections/ToastSection.tsx
Normal file
208
src/components/ui-showcase/sections/ToastSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
// Trouver la tâche sélectionnée
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTaskId && !tasksLoaded) {
|
if (selectedTaskId) {
|
||||||
setTasksLoading(true);
|
const task = tasks.find((t) => t.id === selectedTaskId);
|
||||||
tasksClient
|
setSelectedTask(task);
|
||||||
.getTasks()
|
} else {
|
||||||
.then((response: { data: Task[] }) => {
|
setSelectedTask(undefined);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}, [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>
|
||||||
|
|||||||
79
src/hooks/useTaskSelector.ts
Normal file
79
src/hooks/useTaskSelector.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user