- Added theme context and provider for light/dark mode support. - Integrated theme toggle button in the Header component. - Updated UI components to utilize CSS variables for consistent theming. - Enhanced Kanban components and forms with new theme styles for better visual coherence. - Adjusted global styles to define color variables for both themes, improving maintainability.
198 lines
7.1 KiB
TypeScript
198 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { TagInput } from '@/components/ui/TagInput';
|
|
import { TaskStatus, TaskPriority } from '@/lib/types';
|
|
import { CreateTaskData } from '@/clients/tasks-client';
|
|
import { getAllPriorities, getPriorityConfig } from '@/lib/status-config';
|
|
|
|
interface QuickAddTaskProps {
|
|
status: TaskStatus;
|
|
onSubmit: (data: CreateTaskData) => Promise<void>;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export function QuickAddTask({ status, onSubmit, onCancel }: QuickAddTaskProps) {
|
|
const [formData, setFormData] = useState<CreateTaskData>({
|
|
title: '',
|
|
description: '',
|
|
status,
|
|
priority: 'medium' as TaskPriority,
|
|
tags: [],
|
|
dueDate: undefined
|
|
});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [activeField, setActiveField] = useState<'title' | 'description' | 'tags' | 'date' | null>('title');
|
|
const titleRef = useRef<HTMLInputElement>(null);
|
|
|
|
// Focus automatique sur le titre
|
|
useEffect(() => {
|
|
titleRef.current?.focus();
|
|
}, []);
|
|
|
|
const handleSubmit = async () => {
|
|
const trimmedTitle = formData.title.trim();
|
|
console.log('handleSubmit called:', { trimmedTitle, isSubmitting });
|
|
if (!trimmedTitle || isSubmitting) return;
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
console.log('Submitting task:', { ...formData, title: trimmedTitle });
|
|
await onSubmit({
|
|
...formData,
|
|
title: trimmedTitle
|
|
});
|
|
|
|
// Réinitialiser pour la prochaine tâche
|
|
setFormData({
|
|
title: '',
|
|
description: '',
|
|
status,
|
|
priority: 'medium',
|
|
tags: [],
|
|
dueDate: undefined
|
|
});
|
|
setActiveField('title');
|
|
setIsSubmitting(false);
|
|
titleRef.current?.focus();
|
|
} catch (error) {
|
|
console.error('Erreur lors de la création:', error);
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
|
console.log('Key pressed:', e.key, 'field:', field, 'title:', formData.title);
|
|
|
|
// Seulement intercepter les touches spécifiques qu'on veut gérer
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (field === 'title' && formData.title.trim()) {
|
|
console.log('Calling handleSubmit from title');
|
|
handleSubmit();
|
|
} else if (field === 'tags') {
|
|
// TagInput gère ses propres événements Enter
|
|
} else if (formData.title.trim()) {
|
|
// Permettre création depuis n'importe quel champ si titre rempli
|
|
console.log('Calling handleSubmit from other field');
|
|
handleSubmit();
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
onCancel();
|
|
} else if (e.key === 'Tab' && !e.metaKey && !e.ctrlKey) {
|
|
// Navigation entre les champs seulement si pas de modificateur
|
|
e.preventDefault();
|
|
const fields = ['title', 'description', 'tags', 'date'];
|
|
const currentIndex = fields.indexOf(activeField || 'title');
|
|
const nextField = fields[(currentIndex + 1) % fields.length] as typeof activeField;
|
|
setActiveField(nextField);
|
|
}
|
|
// Laisser passer tous les autres événements (y compris les raccourcis système)
|
|
};
|
|
|
|
const handleTagsChange = (tags: string[]) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
tags
|
|
}));
|
|
};
|
|
|
|
const handleBlur = (e: React.FocusEvent) => {
|
|
// Vérifier si le focus reste dans le composant
|
|
setTimeout(() => {
|
|
const currentTarget = e.currentTarget;
|
|
const relatedTarget = e.relatedTarget as Node | null;
|
|
|
|
// Si le focus sort complètement du composant ET qu'il n'y a pas de titre
|
|
if (currentTarget && (!relatedTarget || !currentTarget.contains(relatedTarget)) && !formData.title.trim()) {
|
|
onCancel();
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
return (
|
|
<div onBlur={handleBlur}>
|
|
<Card className="p-3 border-dashed border-[var(--primary)]/30 bg-[var(--card)]/50 hover:border-[var(--primary)]/50 transition-all duration-300">
|
|
{/* Header avec titre et priorité */}
|
|
<div className="flex items-start gap-2 mb-2">
|
|
<input
|
|
ref={titleRef}
|
|
type="text"
|
|
value={formData.title}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
|
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
|
onFocus={() => setActiveField('title')}
|
|
placeholder="Titre de la tâche..."
|
|
disabled={isSubmitting}
|
|
className="flex-1 bg-transparent border-none outline-none text-[var(--foreground)] font-mono text-sm font-medium placeholder-[var(--muted-foreground)] leading-tight"
|
|
/>
|
|
|
|
{/* Indicateur de priorité */}
|
|
<select
|
|
value={formData.priority}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
|
|
disabled={isSubmitting}
|
|
className="bg-transparent border-none outline-none text-xs font-mono text-[var(--muted-foreground)]"
|
|
>
|
|
{getAllPriorities().map(priorityConfig => (
|
|
<option key={priorityConfig.key} value={priorityConfig.key}>
|
|
{priorityConfig.icon} {priorityConfig.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
|
onKeyDown={(e) => handleKeyDown(e, 'description')}
|
|
onFocus={() => setActiveField('description')}
|
|
placeholder="Description..."
|
|
rows={2}
|
|
disabled={isSubmitting}
|
|
className="w-full bg-transparent border-none outline-none text-xs text-[var(--muted-foreground)] font-mono placeholder-[var(--muted-foreground)] resize-none mb-2"
|
|
/>
|
|
|
|
{/* Tags */}
|
|
<div className="mb-2">
|
|
<TagInput
|
|
tags={formData.tags || []}
|
|
onChange={handleTagsChange}
|
|
placeholder="Tags..."
|
|
maxTags={5}
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* Footer avec date et actions */}
|
|
<div className="pt-2 border-t border-[var(--border)]/50">
|
|
<div className="flex items-center justify-between text-xs min-w-0">
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
|
|
onChange={(e) => setFormData(prev => ({
|
|
...prev,
|
|
dueDate: e.target.value ? new Date(e.target.value) : undefined
|
|
}))}
|
|
onFocus={() => setActiveField('date')}
|
|
disabled={isSubmitting}
|
|
className="bg-transparent border-none outline-none text-[var(--muted-foreground)] font-mono text-xs flex-shrink min-w-0"
|
|
/>
|
|
|
|
{isSubmitting && (
|
|
<div className="flex items-center gap-1 text-[var(--primary)] font-mono text-xs flex-shrink-0">
|
|
<div className="w-3 h-3 border border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
|
|
<span>...</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|