feat: enhance QuickAddTask component with new UI elements

- Replaced input fields with `FormField`, `PrioritySelector`, and `DateTimeInput` for improved user experience and consistency.
- Integrated `LoadingSpinner` to indicate submission state, enhancing feedback during task creation.
- Streamlined state management for form fields, ensuring better data handling.
This commit is contained in:
Julien Froidefond
2025-09-30 10:11:44 +02:00
parent d1d65cdca1
commit 270a2bd4d0
5 changed files with 202 additions and 34 deletions

View File

@@ -3,10 +3,12 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { TagInput } from '@/components/ui/TagInput'; import { TagInput } from '@/components/ui/TagInput';
import { FormField } from '@/components/ui/FormField';
import { PrioritySelector } from '@/components/ui/PrioritySelector';
import { DateTimeInput } from '@/components/ui/DateTimeInput';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { TaskStatus, TaskPriority } from '@/lib/types'; import { TaskStatus, TaskPriority } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface QuickAddTaskProps { interface QuickAddTaskProps {
status: TaskStatus; status: TaskStatus;
@@ -133,44 +135,38 @@ export function QuickAddTask({ status, onSubmit, onCancel, swimlaneContext }: Qu
<Card className="p-3 border-dashed border-[var(--primary)]/30 bg-[var(--card)]/50 hover:border-[var(--primary)]/50 transition-all duration-300"> <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é */} {/* Header avec titre et priorité */}
<div className="flex items-start gap-2 mb-2 min-w-0"> <div className="flex items-start gap-2 mb-2 min-w-0">
<input <FormField
ref={titleRef} ref={titleRef}
type="text" type="text"
value={formData.title} value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))} onChange={(value) => setFormData(prev => ({ ...prev, title: value }))}
onKeyDown={(e) => handleKeyDown(e, 'title')} onKeyDown={(e) => handleKeyDown(e, 'title')}
onFocus={() => setActiveField('title')} onFocus={() => setActiveField('title')}
placeholder="Titre de la tâche..." placeholder="Titre de la tâche..."
disabled={isSubmitting} disabled={isSubmitting}
className="flex-1 min-w-0 bg-transparent border-none outline-none text-[var(--foreground)] font-mono text-sm font-medium placeholder-[var(--muted-foreground)] leading-tight" className="flex-1 min-w-0"
autoFocus
/> />
{/* Indicateur de priorité */} <PrioritySelector
<select value={formData.priority || 'medium'}
value={formData.priority} onChange={(priority) => setFormData(prev => ({ ...prev, priority }))}
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
disabled={isSubmitting} disabled={isSubmitting}
className="flex-shrink-0 w-10 bg-transparent border-none outline-none text-lg text-[var(--muted-foreground)] cursor-pointer text-center" className="flex-shrink-0 w-10"
title={getAllPriorities().find(p => p.key === formData.priority)?.label} />
>
{getAllPriorities().map(priorityConfig => (
<option key={priorityConfig.key} value={priorityConfig.key}>
{priorityConfig.icon}
</option>
))}
</select>
</div> </div>
{/* Description */} {/* Description */}
<textarea <FormField
value={formData.description} type="textarea"
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} value={formData.description || ''}
onChange={(value) => setFormData(prev => ({ ...prev, description: value }))}
onKeyDown={(e) => handleKeyDown(e, 'description')} onKeyDown={(e) => handleKeyDown(e, 'description')}
onFocus={() => setActiveField('description')} onFocus={() => setActiveField('description')}
placeholder="Description..." placeholder="Description..."
rows={2} rows={2}
disabled={isSubmitting} 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" className="mb-2"
/> />
{/* Tags */} {/* Tags */}
@@ -188,23 +184,16 @@ export function QuickAddTask({ status, onSubmit, onCancel, swimlaneContext }: Qu
{/* Footer avec date et actions */} {/* Footer avec date et actions */}
<div className="pt-2 border-t border-[var(--border)]/50"> <div className="pt-2 border-t border-[var(--border)]/50">
<div className="flex items-center justify-between text-xs min-w-0"> <div className="flex items-center justify-between text-xs min-w-0">
<input <DateTimeInput
type="datetime-local" value={formData.dueDate}
value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''} onChange={(date) => setFormData(prev => ({ ...prev, dueDate: date }))}
onChange={(e) => setFormData(prev => ({
...prev,
dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))}
onFocus={() => setActiveField('date')} onFocus={() => setActiveField('date')}
disabled={isSubmitting} disabled={isSubmitting}
className="bg-transparent border-none outline-none text-[var(--muted-foreground)] font-mono text-xs flex-shrink min-w-0" className="flex-shrink min-w-0"
/> />
{isSubmitting && ( {isSubmitting && (
<div className="flex items-center gap-1 text-[var(--primary)] font-mono text-xs flex-shrink-0"> <LoadingSpinner size="sm" text="..." className="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>
</div> </div>

View File

@@ -0,0 +1,38 @@
'use client';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface DateTimeInputProps {
value?: Date;
onChange: (date: Date | undefined) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
onFocus?: () => void;
}
export function DateTimeInput({
value,
onChange,
placeholder,
disabled = false,
className = '',
onFocus
}: DateTimeInputProps) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value ? parseDateTimeInput(e.target.value) : undefined;
onChange(newValue);
};
return (
<input
type="datetime-local"
value={value ? formatDateForDateTimeInput(value) : ''}
onChange={handleChange}
onFocus={onFocus}
disabled={disabled}
placeholder={placeholder}
className={`bg-transparent border-none outline-none text-[var(--muted-foreground)] font-mono text-xs flex-shrink min-w-0 ${className}`}
/>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import { forwardRef } from 'react';
interface FormFieldProps {
type?: 'text' | 'textarea';
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
onKeyDown?: (e: React.KeyboardEvent) => void;
onFocus?: () => void;
rows?: number;
autoFocus?: boolean;
}
export const FormField = forwardRef<HTMLInputElement | HTMLTextAreaElement, FormFieldProps>(
({
type = 'text',
value,
onChange,
placeholder,
disabled = false,
className = '',
onKeyDown,
onFocus,
rows = 2,
autoFocus = false
}, ref) => {
const baseClasses = "bg-transparent border-none outline-none font-mono placeholder-[var(--muted-foreground)]";
if (type === 'textarea') {
return (
<textarea
ref={ref as React.Ref<HTMLTextAreaElement>}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={onKeyDown}
onFocus={onFocus}
placeholder={placeholder}
rows={rows}
disabled={disabled}
autoFocus={autoFocus}
className={`${baseClasses} text-xs text-[var(--muted-foreground)] resize-none ${className}`}
/>
);
}
return (
<input
ref={ref as React.Ref<HTMLInputElement>}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={onKeyDown}
onFocus={onFocus}
placeholder={placeholder}
disabled={disabled}
autoFocus={autoFocus}
className={`${baseClasses} text-[var(--foreground)] text-sm font-medium leading-tight ${className}`}
/>
);
}
);
FormField.displayName = 'FormField';

View File

@@ -0,0 +1,32 @@
'use client';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
text?: string;
}
const sizeClasses = {
sm: 'w-3 h-3',
md: 'w-4 h-4',
lg: 'w-6 h-6'
};
const textSizeClasses = {
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base'
};
export function LoadingSpinner({
size = 'sm',
className = '',
text
}: LoadingSpinnerProps) {
return (
<div className={`flex items-center gap-1 text-[var(--primary)] font-mono ${textSizeClasses[size]} ${className}`}>
<div className={`${sizeClasses[size]} border border-[var(--primary)] border-t-transparent rounded-full animate-spin`}></div>
{text && <span>{text}</span>}
</div>
);
}

View File

@@ -0,0 +1,42 @@
'use client';
import { TaskPriority } from '@/lib/types';
import { getAllPriorities } from '@/lib/status-config';
interface PrioritySelectorProps {
value: TaskPriority;
onChange: (priority: TaskPriority) => void;
disabled?: boolean;
className?: string;
title?: string;
}
export function PrioritySelector({
value,
onChange,
disabled = false,
className = '',
title
}: PrioritySelectorProps) {
const priorities = getAllPriorities();
const currentPriority = priorities.find(p => p.key === value);
return (
<select
value={value}
onChange={(e) => {
const selectedValue = e.target.value as TaskPriority;
if (selectedValue) onChange(selectedValue);
}}
disabled={disabled}
className={`bg-transparent border-none outline-none text-lg text-[var(--muted-foreground)] cursor-pointer text-center ${className}`}
title={title || currentPriority?.label}
>
{priorities.map(priorityConfig => (
<option key={priorityConfig.key} value={priorityConfig.key}>
{priorityConfig.icon}
</option>
))}
</select>
);
}