sauvegarde boite a idée
This commit is contained in:
@@ -4,12 +4,15 @@ import IdeaBoard from '@/components/IdeaBoard';
|
||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||
|
||||
export default function IdeasPage() {
|
||||
const { project, updateProject } = useProjectContext();
|
||||
const { project, projectId, createIdea, updateIdea, deleteIdea } = useProjectContext();
|
||||
|
||||
return (
|
||||
<IdeaBoard
|
||||
projectId={projectId}
|
||||
ideas={project.ideas || []}
|
||||
onUpdate={(ideas) => updateProject({ ideas })}
|
||||
onCreate={(data) => createIdea(projectId, data)}
|
||||
onUpdateIdea={(id, data) => updateIdea(projectId, id, data)}
|
||||
onDelete={(id) => deleteIdea(projectId, id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
||||
const {
|
||||
projects, setCurrentProjectId,
|
||||
updateProject, updateChapter, addChapter,
|
||||
createEntity, updateEntity, deleteEntity, deleteProject
|
||||
createEntity, updateEntity, deleteEntity,
|
||||
createIdea, updateIdea, deleteIdea,
|
||||
deleteProject
|
||||
} = useProjects(user);
|
||||
const { chatHistory, isGenerating, sendMessage } = useChat();
|
||||
|
||||
@@ -111,6 +113,9 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
||||
createEntity: (type, data) => createEntity(projectId, type, data),
|
||||
updateEntity: (entityId, data) => updateEntity(projectId, entityId, data),
|
||||
deleteEntity: (entityId) => deleteEntity(projectId, entityId),
|
||||
createIdea: (projectId, data) => createIdea(projectId, data),
|
||||
updateIdea: (projectId, ideaId, data) => updateIdea(projectId, ideaId, data),
|
||||
deleteIdea: (projectId, ideaId) => deleteIdea(projectId, ideaId),
|
||||
deleteProject: () => deleteProject(projectId),
|
||||
incrementUsage,
|
||||
}}>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types';
|
||||
import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save } from 'lucide-react';
|
||||
import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save, Check, CheckCheck, Loader2 } from 'lucide-react';
|
||||
import { useLanguage } from '@/providers/LanguageProvider';
|
||||
import { TranslationKey } from '@/lib/i18n/translations';
|
||||
|
||||
interface IdeaBoardProps {
|
||||
projectId: string;
|
||||
ideas: Idea[];
|
||||
onUpdate: (ideas: Idea[]) => void;
|
||||
onCreate: (data: Partial<Idea>) => Promise<string>;
|
||||
onUpdateIdea: (id: string, data: Partial<Idea>) => Promise<void>;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const CATEGORIES: Record<IdeaCategory, { labelKey: string, color: string, icon: any }> = {
|
||||
@@ -26,10 +29,26 @@ const STATUS_LABELS: Record<IdeaStatus, string> = {
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 500;
|
||||
|
||||
const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
const IdeaBoard: React.FC<IdeaBoardProps> = ({ projectId, ideas, onCreate, onUpdateIdea, onDelete }) => {
|
||||
const { t } = useLanguage();
|
||||
const [newIdeaTitle, setNewIdeaTitle] = useState('');
|
||||
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
|
||||
const [localIdeas, setLocalIdeas] = useState<Idea[]>(ideas);
|
||||
|
||||
// Auto-Save State
|
||||
const [saveStatus, setSaveStatus] = useState<'saved_local' | 'saved_db' | 'saving' | 'unsaved'>('saved_db');
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Sync from parent
|
||||
useEffect(() => {
|
||||
setLocalIdeas(ideas);
|
||||
}, [ideas]);
|
||||
|
||||
const saveLocally = (updated: Idea[]) => {
|
||||
setLocalIdeas(updated);
|
||||
localStorage.setItem(`ideas_${projectId}`, JSON.stringify(updated));
|
||||
setSaveStatus('saved_local');
|
||||
};
|
||||
|
||||
// Drag and Drop State
|
||||
const [draggedIdeaId, setDraggedIdeaId] = useState<string | null>(null);
|
||||
@@ -39,12 +58,12 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
|
||||
// --- ACTIONS ---
|
||||
|
||||
const handleAddIdea = (e: React.FormEvent) => {
|
||||
const handleAddIdea = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newIdeaTitle.trim()) return;
|
||||
|
||||
const newIdea: Idea = {
|
||||
id: `idea-${Date.now()}`,
|
||||
id: `temp-${Date.now()}`,
|
||||
title: newIdeaTitle,
|
||||
description: '',
|
||||
category: newIdeaCategory,
|
||||
@@ -52,36 +71,86 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
onUpdate([...ideas, newIdea]);
|
||||
saveLocally([...localIdeas, newIdea]);
|
||||
setNewIdeaTitle('');
|
||||
};
|
||||
setSaveStatus('saving');
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (confirm(t('ideaboard.delete') + " ?")) {
|
||||
onUpdate(ideas.filter(i => i.id !== id));
|
||||
if (editingItem?.id === id) setEditingItem(null);
|
||||
try {
|
||||
await onCreate({
|
||||
title: newIdea.title,
|
||||
category: newIdea.category,
|
||||
status: newIdea.status,
|
||||
});
|
||||
setSaveStatus('saved_db');
|
||||
} catch (err) {
|
||||
setSaveStatus('saved_local');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm(t('ideaboard.delete') + " ?")) {
|
||||
saveLocally(localIdeas.filter(i => i.id !== id));
|
||||
if (editingItem?.id === id) setEditingItem(null);
|
||||
|
||||
setSaveStatus('saving');
|
||||
try {
|
||||
if (!id.startsWith('temp-')) {
|
||||
await onDelete(id);
|
||||
}
|
||||
setSaveStatus('saved_db');
|
||||
} catch (err) {
|
||||
setSaveStatus('saved_local');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (!editingItem || !editingItem.title?.trim()) return;
|
||||
|
||||
if (editingItem.id) {
|
||||
let isNew = !editingItem.id || editingItem.id.startsWith('temp-');
|
||||
|
||||
if (!isNew) {
|
||||
// Update existing
|
||||
onUpdate(ideas.map(i => i.id === editingItem.id ? { ...i, ...editingItem } as Idea : i));
|
||||
saveLocally(localIdeas.map(i => i.id === editingItem.id ? { ...i, ...editingItem } as Idea : i));
|
||||
setEditingItem(null);
|
||||
setSaveStatus('saving');
|
||||
try {
|
||||
await onUpdateIdea(editingItem.id!, {
|
||||
title: editingItem.title,
|
||||
description: editingItem.description,
|
||||
category: editingItem.category,
|
||||
status: editingItem.status
|
||||
});
|
||||
setSaveStatus('saved_db');
|
||||
} catch (err) {
|
||||
setSaveStatus('saved_local');
|
||||
}
|
||||
} else {
|
||||
// Create new from modal
|
||||
const newIdea: Idea = {
|
||||
id: `idea-${Date.now()}`,
|
||||
id: `temp-${Date.now()}`,
|
||||
title: editingItem.title || '',
|
||||
description: editingItem.description || '',
|
||||
category: editingItem.category || 'plot',
|
||||
status: editingItem.status || 'todo',
|
||||
createdAt: Date.now()
|
||||
};
|
||||
onUpdate([...ideas, newIdea]);
|
||||
saveLocally([...localIdeas, newIdea]);
|
||||
setEditingItem(null);
|
||||
|
||||
setSaveStatus('saving');
|
||||
try {
|
||||
await onCreate({
|
||||
title: newIdea.title,
|
||||
description: newIdea.description,
|
||||
category: newIdea.category,
|
||||
status: newIdea.status,
|
||||
});
|
||||
setSaveStatus('saved_db');
|
||||
} catch (err) {
|
||||
setSaveStatus('saved_local');
|
||||
}
|
||||
}
|
||||
setEditingItem(null);
|
||||
};
|
||||
|
||||
const openQuickAdd = (status: IdeaStatus) => {
|
||||
@@ -104,14 +173,24 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, status: IdeaStatus) => {
|
||||
const handleDrop = async (e: React.DragEvent, status: IdeaStatus) => {
|
||||
e.preventDefault();
|
||||
if (draggedIdeaId) {
|
||||
const updatedIdeas = ideas.map(idea =>
|
||||
saveLocally(localIdeas.map(idea =>
|
||||
idea.id === draggedIdeaId ? { ...idea, status } : idea
|
||||
);
|
||||
onUpdate(updatedIdeas);
|
||||
));
|
||||
const idToUpdate = draggedIdeaId;
|
||||
setDraggedIdeaId(null);
|
||||
|
||||
if (!idToUpdate.startsWith('temp-')) {
|
||||
setSaveStatus('saving');
|
||||
try {
|
||||
await onUpdateIdea(idToUpdate, { status });
|
||||
setSaveStatus('saved_db');
|
||||
} catch (err) {
|
||||
setSaveStatus('saved_local');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,7 +202,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
// --- RENDERERS ---
|
||||
|
||||
const Column = ({ title, status, icon: Icon }: { title: string, status: IdeaStatus, icon: any }) => {
|
||||
const columnIdeas = ideas.filter(i => i.status === status);
|
||||
const columnIdeas = localIdeas.filter(i => i.status === status);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -227,38 +306,56 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
|
||||
{/* Header & Add Form (Top Bar) */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-theme-panel p-4 rounded-xl border border-theme-border shadow-sm shrink-0 transition-colors duration-300">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2">
|
||||
<Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
|
||||
</h2>
|
||||
<p className="text-theme-muted text-sm">{t('ideaboard.desc')}</p>
|
||||
<div className="flex justify-between items-center w-full md:w-auto">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-theme-text flex items-center gap-2">
|
||||
<Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
|
||||
</h2>
|
||||
<p className="text-theme-muted text-xs">{t('ideaboard.desc')}</p>
|
||||
</div>
|
||||
{/* Status Indicator for Mobile */}
|
||||
<div className="md:hidden flex items-center gap-2 text-xs font-medium text-slate-400">
|
||||
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /></>}
|
||||
{saveStatus === 'saved_local' && <><Check size={14} className="text-green-500" /></>}
|
||||
{saveStatus === 'saved_db' && <><CheckCheck size={14} className="text-emerald-600" /></>}
|
||||
</div>
|
||||
</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-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none transition-colors duration-300"
|
||||
>
|
||||
{Object.entries(CATEGORIES).map(([key, val]) => (
|
||||
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={newIdeaTitle}
|
||||
onChange={(e) => setNewIdeaTitle(e.target.value)}
|
||||
placeholder={t('ideaboard.add_idea')}
|
||||
className="flex-1 bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none font-medium transition-colors duration-300"
|
||||
/>
|
||||
<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 className="flex items-center gap-4 w-full md:w-auto flex-1 justify-end">
|
||||
{/* Status Indicator for Desktop */}
|
||||
<div className="hidden md:flex items-center gap-2 text-xs font-medium text-slate-400">
|
||||
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /> <span className="text-blue-500">Sauvegarde...</span></>}
|
||||
{saveStatus === 'saved_local' && <><Check size={14} className="text-green-500" /> <span className="text-green-500">Brouillon local</span></>}
|
||||
{saveStatus === 'saved_db' && <><CheckCheck size={14} className="text-emerald-600" /> <span className="text-emerald-600">Sauvegardé</span></>}
|
||||
{saveStatus === 'unsaved' && <span className="text-amber-500">Non sauvegardé...</span>}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-lg flex gap-2">
|
||||
<select
|
||||
value={newIdeaCategory}
|
||||
onChange={(e) => setNewIdeaCategory(e.target.value as IdeaCategory)}
|
||||
className="bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none transition-colors duration-300"
|
||||
>
|
||||
{Object.entries(CATEGORIES).map(([key, val]) => (
|
||||
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={newIdeaTitle}
|
||||
onChange={(e) => setNewIdeaTitle(e.target.value)}
|
||||
placeholder={t('ideaboard.add_idea')}
|
||||
className="flex-1 bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none font-medium transition-colors duration-300"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Kanban Board */}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Entity,
|
||||
EntityType,
|
||||
UserProfile,
|
||||
Idea
|
||||
} from '@/lib/types';
|
||||
import api from '@/lib/api';
|
||||
import {
|
||||
@@ -293,6 +294,67 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createIdea = async (projectId: string, data: Partial<Idea>) => {
|
||||
try {
|
||||
const newIdea = await api.ideas.create({
|
||||
projectId,
|
||||
title: data.title || 'Nouveau',
|
||||
description: data.description || '',
|
||||
status: data.status || 'todo',
|
||||
category: data.category || 'plot',
|
||||
});
|
||||
|
||||
setProjects(prev => prev.map(p => {
|
||||
if (p.id !== projectId) return p;
|
||||
return {
|
||||
...p,
|
||||
ideas: [...(p.ideas || []), {
|
||||
...newIdea,
|
||||
createdAt: new Date(newIdea.createdAt).getTime()
|
||||
}]
|
||||
};
|
||||
}));
|
||||
return newIdea.id;
|
||||
} catch (err) {
|
||||
console.error("Failed to create idea", err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateIdea = async (projectId: string, ideaId: string, data: Partial<Idea>) => {
|
||||
setProjects(prev => prev.map(p => {
|
||||
if (p.id !== projectId) return p;
|
||||
return {
|
||||
...p,
|
||||
ideas: (p.ideas || []).map(i => i.id === ideaId ? { ...i, ...data } : i)
|
||||
};
|
||||
}));
|
||||
|
||||
try {
|
||||
await api.ideas.update(ideaId, data);
|
||||
} catch (err) {
|
||||
console.error("Failed to update idea", err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteIdea = async (projectId: string, ideaId: string) => {
|
||||
setProjects(prev => prev.map(p => {
|
||||
if (p.id !== projectId) return p;
|
||||
return {
|
||||
...p,
|
||||
ideas: (p.ideas || []).filter(i => i.id !== ideaId)
|
||||
};
|
||||
}));
|
||||
|
||||
try {
|
||||
await api.ideas.delete(ideaId);
|
||||
} catch (err) {
|
||||
console.error("Failed to delete idea", err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
projects,
|
||||
currentProjectId,
|
||||
@@ -304,6 +366,9 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
createEntity,
|
||||
updateEntity,
|
||||
deleteEntity,
|
||||
createIdea,
|
||||
updateIdea,
|
||||
deleteIdea,
|
||||
deleteProject: async (projectId: string) => {
|
||||
try {
|
||||
// Cascade delete is handled by Prisma, just delete the project
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { BookProject, UserProfile, Entity, EntityType } from '@/lib/types';
|
||||
import { BookProject, UserProfile, Entity, EntityType, Idea } from '@/lib/types';
|
||||
|
||||
interface ProjectContextType {
|
||||
project: BookProject;
|
||||
@@ -14,6 +14,9 @@ interface ProjectContextType {
|
||||
createEntity: (type: EntityType, initialData?: Partial<Entity>) => Promise<string>;
|
||||
updateEntity: (entityId: string, data: Partial<Entity>) => Promise<void>;
|
||||
deleteEntity: (entityId: string) => Promise<void>;
|
||||
createIdea: (projectId: string, data: Partial<Idea>) => Promise<string>;
|
||||
updateIdea: (projectId: string, ideaId: string, data: Partial<Idea>) => Promise<void>;
|
||||
deleteIdea: (projectId: string, ideaId: string) => Promise<void>;
|
||||
deleteProject: () => Promise<void>;
|
||||
incrementUsage: () => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user