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 { 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({
|
||||
<span className="text-sm font-medium text-[var(--foreground)]">
|
||||
Tâche:
|
||||
</span>
|
||||
<TaskSelector
|
||||
<TaskSelectorWithData
|
||||
selectedTaskId={selectedTaskId}
|
||||
onTaskSelect={onTaskChange}
|
||||
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,
|
||||
DataDisplaySection,
|
||||
DropdownsSection,
|
||||
IconsSection,
|
||||
TaskSelectorSection,
|
||||
ToastSection,
|
||||
} from './sections';
|
||||
|
||||
export function UIShowcaseClient() {
|
||||
@@ -41,6 +44,9 @@ export function UIShowcaseClient() {
|
||||
<NavigationSection />
|
||||
<FeedbackSection />
|
||||
<DataDisplaySection />
|
||||
<IconsSection />
|
||||
<TaskSelectorSection />
|
||||
<ToastSection />
|
||||
</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 { DataDisplaySection } from './DataDisplaySection';
|
||||
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 { 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<Task[]>([]);
|
||||
const [tasksLoading, setTasksLoading] = useState(false);
|
||||
const [tasksLoaded, setTasksLoaded] = useState(false); // Nouvel état pour tracker le chargement
|
||||
const [taskSearch, setTaskSearch] = useState('');
|
||||
const [selectedTask, setSelectedTask] = useState<Task | undefined>(undefined);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||
@@ -35,23 +35,15 @@ export function TaskSelector({
|
||||
const containerRef = 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(() => {
|
||||
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 */}
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{tasksLoading ? (
|
||||
{loading ? (
|
||||
<div className="p-3 text-center text-[var(--muted-foreground)]">
|
||||
Chargement...
|
||||
</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