authentification nocodebackend ok
This commit is contained in:
371
components/IdeaBoard.tsx
Normal file
371
components/IdeaBoard.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user