Files
plume/components/IdeaBoard.tsx

371 lines
17 KiB
TypeScript

import React, { useState } from 'react';
import { Idea, IdeaStatus, IdeaCategory } from '../types';
import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save } from 'lucide-react';
interface IdeaBoardProps {
ideas: Idea[];
onUpdate: (ideas: Idea[]) => void;
}
const CATEGORIES: Record<IdeaCategory, { label: string, color: string, icon: any }> = {
plot: { label: 'Intrigue', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Lightbulb },
character: { label: 'Personnage', color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Search },
research: { label: 'Recherche', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Search },
todo: { label: 'À faire', color: 'bg-slate-100 text-slate-800 border-slate-200', icon: CheckCircle },
};
const STATUS_LABELS: Record<IdeaStatus, string> = {
todo: 'Idées / À faire',
progress: 'En cours',
done: 'Terminé / Validé'
};
const MAX_DESCRIPTION_LENGTH = 500;
const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
const [newIdeaTitle, setNewIdeaTitle] = useState('');
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
// Drag and Drop State
const [draggedIdeaId, setDraggedIdeaId] = useState<string | null>(null);
// Modal State for Edit/Quick Add
const [editingItem, setEditingItem] = useState<Partial<Idea> | null>(null);
// --- ACTIONS ---
const handleAddIdea = (e: React.FormEvent) => {
e.preventDefault();
if (!newIdeaTitle.trim()) return;
const newIdea: Idea = {
id: `idea-${Date.now()}`,
title: newIdeaTitle,
description: '',
category: newIdeaCategory,
status: 'todo',
createdAt: Date.now()
};
onUpdate([...ideas, newIdea]);
setNewIdeaTitle('');
};
const handleDelete = (id: string) => {
if(confirm("Supprimer cette carte ?")) {
onUpdate(ideas.filter(i => i.id !== id));
if (editingItem?.id === id) setEditingItem(null);
}
};
const handleSaveEdit = () => {
if (!editingItem || !editingItem.title?.trim()) return;
if (editingItem.id) {
// Update existing
onUpdate(ideas.map(i => i.id === editingItem.id ? { ...i, ...editingItem } as Idea : i));
} else {
// Create new from modal
const newIdea: Idea = {
id: `idea-${Date.now()}`,
title: editingItem.title || '',
description: editingItem.description || '',
category: editingItem.category || 'plot',
status: editingItem.status || 'todo',
createdAt: Date.now()
};
onUpdate([...ideas, newIdea]);
}
setEditingItem(null);
};
const openQuickAdd = (status: IdeaStatus) => {
setEditingItem({
title: '',
description: '',
category: 'plot',
status: status
});
};
const openEdit = (idea: Idea) => {
setEditingItem({ ...idea });
};
// --- DRAG HANDLERS ---
const handleDragStart = (e: React.DragEvent, id: string) => {
setDraggedIdeaId(id);
e.dataTransfer.effectAllowed = 'move';
};
const handleDrop = (e: React.DragEvent, status: IdeaStatus) => {
e.preventDefault();
if (draggedIdeaId) {
const updatedIdeas = ideas.map(idea =>
idea.id === draggedIdeaId ? { ...idea, status } : idea
);
onUpdate(updatedIdeas);
setDraggedIdeaId(null);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
// --- RENDERERS ---
const Column = ({ title, status, icon: Icon }: { title: string, status: IdeaStatus, icon: any }) => {
const columnIdeas = ideas.filter(i => i.status === status);
return (
<div
className="flex-1 bg-[#eef2ff] rounded-xl border border-indigo-100 flex flex-col h-full overflow-hidden transition-colors hover:border-blue-200"
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, status)}
onDoubleClick={() => openQuickAdd(status)}
title="Double-cliquez dans le vide pour ajouter une carte ici"
>
{/* Column Header */}
<div className={`p-4 border-b border-indigo-200 flex justify-between items-center ${
status === 'todo' ? 'bg-[#eef2ff]' :
status === 'progress' ? 'bg-indigo-50' :
'bg-green-50'
}`}>
<div className="flex items-center gap-2 font-bold text-slate-700">
<Icon size={18} />
{title}
</div>
<div className="flex items-center gap-2">
<button
onClick={(e) => { e.stopPropagation(); openQuickAdd(status); }}
className="p-1 hover:bg-white rounded-full text-slate-400 hover:text-blue-600 transition-colors"
>
<Plus size={16} />
</button>
<span className="text-xs font-semibold bg-white px-2 py-1 rounded-full border border-indigo-100 text-slate-500">
{columnIdeas.length}
</span>
</div>
</div>
{/* Column Body */}
<div className="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
{columnIdeas.map(idea => {
const truncatedDesc = idea.description.length > 300
? idea.description.substring(0, 300) + '...'
: idea.description;
return (
<div
key={idea.id}
draggable
onDragStart={(e) => handleDragStart(e, idea.id)}
onDoubleClick={(e) => {
e.stopPropagation(); // Prevent column double-click
openEdit(idea);
}}
className="bg-white p-3 rounded-lg shadow-sm border border-slate-200 cursor-grab active:cursor-grabbing hover:shadow-md hover:border-blue-300 transition-all group relative animate-in zoom-in-95 duration-200"
>
<div className="flex justify-between items-start mb-2">
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full flex items-center gap-1 ${CATEGORIES[idea.category].color}`}>
{CATEGORIES[idea.category].label}
</span>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => { e.stopPropagation(); openEdit(idea); }}
className="text-slate-300 hover:text-blue-500"
>
<Edit3 size={14} />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDelete(idea.id); }}
className="text-slate-300 hover:text-red-500"
>
<Trash2 size={14} />
</button>
</div>
</div>
{/* CARD CONTENT */}
<div className="mb-2">
<h4 className="font-bold text-slate-800 text-sm mb-1 leading-tight">{idea.title}</h4>
{idea.description && (
<p className="text-xs text-slate-500 line-clamp-3 leading-relaxed" title={idea.description.length > 300 ? "Description tronquée (voir détail)" : undefined}>
{truncatedDesc}
</p>
)}
</div>
<div className="flex justify-between items-center text-xs text-slate-400 border-t border-slate-50 pt-2 mt-2">
<span className="flex items-center gap-1">
<Clock size={10} /> {new Date(idea.createdAt).toLocaleDateString()}
</span>
<GripVertical size={14} className="opacity-20" />
</div>
</div>
);
})}
{columnIdeas.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-slate-300 text-sm italic border-2 border-dashed border-indigo-200 rounded-lg m-1">
<span className="mb-2">Vide</span>
<span className="text-xs opacity-70">Double-cliquez pour ajouter</span>
</div>
)}
</div>
</div>
);
};
return (
<div className="flex flex-col h-full bg-white p-6 gap-6 relative">
{/* Header & Add Form (Top Bar) */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-white p-4 rounded-xl border border-slate-200 shadow-sm shrink-0">
<div>
<h2 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
<Lightbulb className="text-yellow-500" /> Boîte à Idées
</h2>
<p className="text-slate-500 text-sm">Organisez vos tâches, idées de scènes et recherches.</p>
</div>
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-2xl flex gap-2">
<select
value={newIdeaCategory}
onChange={(e) => setNewIdeaCategory(e.target.value as IdeaCategory)}
className="bg-[#eef2ff] border border-indigo-200 text-slate-700 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none"
>
{Object.entries(CATEGORIES).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option>
))}
</select>
<input
type="text"
value={newIdeaTitle}
onChange={(e) => setNewIdeaTitle(e.target.value)}
placeholder="Titre de la nouvelle idée..."
className="flex-1 bg-[#eef2ff] border border-indigo-200 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none font-medium"
/>
<button
type="submit"
disabled={!newIdeaTitle.trim()}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 disabled:opacity-50 transition-colors flex items-center gap-2"
>
<Plus size={18} />
</button>
</form>
</div>
{/* Kanban Board */}
<div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-6 min-h-0">
<Column title="Idées / À faire" status="todo" icon={Circle} />
<Column title="En cours" status="progress" icon={Clock} />
<Column title="Terminé" status="done" icon={CheckCircle} />
</div>
{/* EDIT / QUICK ADD MODAL */}
{editingItem && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg overflow-hidden flex flex-col max-h-[90%]">
<div className="bg-[#eef2ff] border-b border-indigo-100 p-4 flex justify-between items-center">
<h3 className="font-bold text-slate-700 flex items-center gap-2">
{editingItem.id ? <Edit3 size={18}/> : <Plus size={18}/>}
{editingItem.id ? 'Éditer la carte' : 'Ajouter une carte'}
</h3>
<button onClick={() => setEditingItem(null)} className="text-slate-400 hover:text-slate-600">
<X size={20} />
</button>
</div>
<div className="p-6 space-y-4 overflow-y-auto">
<div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Titre</label>
<input
type="text"
value={editingItem.title}
onChange={(e) => setEditingItem({...editingItem, title: e.target.value})}
className="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-bold text-slate-800"
placeholder="Titre de la tâche ou de l'idée..."
autoFocus
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Description</label>
<textarea
value={editingItem.description}
onChange={(e) => setEditingItem({...editingItem, description: e.target.value})}
maxLength={MAX_DESCRIPTION_LENGTH}
className="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none min-h-[120px] text-sm text-slate-600 leading-relaxed resize-none"
placeholder="Détails, notes, liens..."
/>
<div className={`text-right text-xs mt-1 transition-colors ${
(editingItem.description?.length || 0) >= MAX_DESCRIPTION_LENGTH ? 'text-red-500 font-bold' :
(editingItem.description?.length || 0) > MAX_DESCRIPTION_LENGTH * 0.9 ? 'text-orange-500' : 'text-slate-400'
}`}>
{editingItem.description?.length || 0} / {MAX_DESCRIPTION_LENGTH} caractères
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Catégorie</label>
<select
value={editingItem.category}
onChange={(e) => setEditingItem({...editingItem, category: e.target.value as IdeaCategory})}
className="w-full p-2 bg-white border border-slate-300 rounded-lg text-sm outline-none focus:border-blue-500"
>
{Object.entries(CATEGORIES).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Statut</label>
<select
value={editingItem.status}
onChange={(e) => setEditingItem({...editingItem, status: e.target.value as IdeaStatus})}
className="w-full p-2 bg-white border border-slate-300 rounded-lg text-sm outline-none focus:border-blue-500"
>
{Object.entries(STATUS_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val}</option>
))}
</select>
</div>
</div>
</div>
<div className="p-4 border-t border-slate-200 bg-[#eef2ff] flex justify-end gap-2 shrink-0">
{editingItem.id && (
<button
onClick={() => handleDelete(editingItem.id!)}
className="mr-auto text-red-500 hover:text-red-700 text-sm font-medium px-3 py-2"
>
Supprimer
</button>
)}
<button
onClick={() => setEditingItem(null)}
className="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg text-sm font-medium"
>
Annuler
</button>
<button
onClick={handleSaveEdit}
disabled={!editingItem.title?.trim()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium shadow-sm disabled:opacity-50 flex items-center gap-2"
>
<Save size={16} /> Enregistrer
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default IdeaBoard;