feat: refactor Daily components and enhance UI integration
- Replaced `DailyCalendar` with a new `Calendar` component for improved functionality and consistency. - Introduced `AlertBanner` to replace `DeadlineReminder`, providing a more flexible way to display urgent tasks. - Updated `DailyAddForm` to use new options for task types, enhancing user experience when adding tasks. - Removed unused state and components, streamlining the DailyPageClient for better performance and maintainability. - Enhanced `DailySection` to utilize new `CheckboxItem` format for better integration with the UI. - Cleaned up imports and improved overall structure for better readability.
This commit is contained in:
193
src/components/ui/CheckboxItem.tsx
Normal file
193
src/components/ui/CheckboxItem.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
|
||||
export interface CheckboxItemData {
|
||||
id: string;
|
||||
text: string;
|
||||
isChecked: boolean;
|
||||
type?: 'task' | 'meeting' | string;
|
||||
taskId?: string;
|
||||
task?: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckboxItemProps {
|
||||
item: CheckboxItemData;
|
||||
onToggle: (itemId: string) => Promise<void>;
|
||||
onUpdate: (itemId: string, text: string, type?: string, taskId?: string) => Promise<void>;
|
||||
onDelete: (itemId: string) => Promise<void>;
|
||||
saving?: boolean;
|
||||
showTypeIndicator?: boolean;
|
||||
showTaskLink?: boolean;
|
||||
showEditButton?: boolean;
|
||||
showDeleteButton?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CheckboxItem({
|
||||
item,
|
||||
onToggle,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
saving = false,
|
||||
showTaskLink = true,
|
||||
showEditButton = true,
|
||||
showDeleteButton = true,
|
||||
className = ''
|
||||
}: CheckboxItemProps) {
|
||||
const [inlineEditingId, setInlineEditingId] = useState<string | null>(null);
|
||||
const [inlineEditingText, setInlineEditingText] = useState('');
|
||||
const [optimisticChecked, setOptimisticChecked] = useState<boolean | null>(null);
|
||||
|
||||
// État optimiste local pour une réponse immédiate
|
||||
const isChecked = optimisticChecked !== null ? optimisticChecked : item.isChecked;
|
||||
|
||||
// Synchroniser l'état optimiste avec les changements externes
|
||||
useEffect(() => {
|
||||
if (optimisticChecked !== null && optimisticChecked === item.isChecked) {
|
||||
// L'état serveur a été mis à jour, on peut reset l'optimiste
|
||||
setOptimisticChecked(null);
|
||||
}
|
||||
}, [item.isChecked, optimisticChecked]);
|
||||
|
||||
// Handler optimiste pour le toggle
|
||||
const handleOptimisticToggle = async () => {
|
||||
const newCheckedState = !isChecked;
|
||||
|
||||
// Mise à jour optimiste immédiate
|
||||
setOptimisticChecked(newCheckedState);
|
||||
|
||||
try {
|
||||
await onToggle(item.id);
|
||||
// Reset l'état optimiste après succès
|
||||
setOptimisticChecked(null);
|
||||
} catch (error) {
|
||||
// Rollback en cas d'erreur
|
||||
setOptimisticChecked(null);
|
||||
console.error('Erreur lors du toggle:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Édition inline simple
|
||||
const handleStartInlineEdit = () => {
|
||||
setInlineEditingId(item.id);
|
||||
setInlineEditingText(item.text);
|
||||
};
|
||||
|
||||
const handleSaveInlineEdit = async () => {
|
||||
if (!inlineEditingText.trim()) return;
|
||||
|
||||
try {
|
||||
await onUpdate(item.id, inlineEditingText.trim(), item.type, item.taskId);
|
||||
setInlineEditingId(null);
|
||||
setInlineEditingText('');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la modification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelInlineEdit = () => {
|
||||
setInlineEditingId(null);
|
||||
setInlineEditingText('');
|
||||
};
|
||||
|
||||
const handleInlineEditKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSaveInlineEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancelInlineEdit();
|
||||
}
|
||||
};
|
||||
|
||||
// Obtenir la couleur de bordure selon le type
|
||||
const getTypeBorderColor = () => {
|
||||
if (item.type === 'meeting') return 'border-l-blue-500';
|
||||
return 'border-l-green-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 px-3 py-2 sm:py-1.5 sm:gap-2 rounded border transition-colors group border-l-4 ${getTypeBorderColor()} border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)] ${className}`}>
|
||||
{/* Checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={handleOptimisticToggle}
|
||||
disabled={saving}
|
||||
className="w-4 h-4 md:w-3.5 md:h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1"
|
||||
/>
|
||||
|
||||
{/* Contenu principal */}
|
||||
{inlineEditingId === item.id ? (
|
||||
<Input
|
||||
value={inlineEditingText}
|
||||
onChange={(e) => setInlineEditingText(e.target.value)}
|
||||
onKeyDown={handleInlineEditKeyPress}
|
||||
onBlur={handleSaveInlineEdit}
|
||||
autoFocus
|
||||
className="flex-1 h-7 text-sm"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
{/* Texte cliquable pour édition inline */}
|
||||
<span
|
||||
className={`flex-1 text-sm sm:text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${
|
||||
item.isChecked
|
||||
? 'line-through text-[var(--muted-foreground)]'
|
||||
: 'text-[var(--foreground)]'
|
||||
}`}
|
||||
onClick={handleStartInlineEdit}
|
||||
title="Cliquer pour éditer le texte"
|
||||
>
|
||||
{item.text}
|
||||
</span>
|
||||
|
||||
{/* Icône d'édition avancée */}
|
||||
{showEditButton && (
|
||||
<button
|
||||
onClick={() => {
|
||||
// Pour l'instant, on utilise l'édition inline
|
||||
// Plus tard, on pourra ajouter une modal d'édition avancée
|
||||
handleStartInlineEdit();
|
||||
}}
|
||||
disabled={saving}
|
||||
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--muted)]/50 hover:bg-[var(--muted)] border border-[var(--border)]/30 hover:border-[var(--border)] flex items-center justify-center transition-all duration-200 text-[var(--foreground)] text-xs"
|
||||
title="Éditer le texte"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lien vers la tâche si liée */}
|
||||
{showTaskLink && item.task && (
|
||||
<Link
|
||||
href={`/kanban?taskId=${item.task.id}`}
|
||||
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono truncate max-w-[100px]"
|
||||
title={`Tâche: ${item.task.title}`}
|
||||
>
|
||||
{item.task.title}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Bouton de suppression */}
|
||||
{showDeleteButton && (
|
||||
<button
|
||||
onClick={() => onDelete(item.id)}
|
||||
disabled={saving}
|
||||
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--destructive)]/20 hover:bg-[var(--destructive)]/30 border border-[var(--destructive)]/30 hover:border-[var(--destructive)]/50 flex items-center justify-center transition-all duration-200 text-[var(--destructive)] text-xs"
|
||||
title="Supprimer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user