From d004281e052ae93f329d2b99580d4ec4359d9c95 Mon Sep 17 00:00:00 2001 From: streaper2 Date: Thu, 5 Mar 2026 12:47:36 +0100 Subject: [PATCH] =?UTF-8?q?sauvegarde=20boite=20a=20id=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 79 +------- .next/dev/cache/turbopack/23c46498/CURRENT | Bin 4 -> 4 bytes .next/dev/cache/turbopack/23c46498/LOG | 6 + next-env.d.ts | 2 +- src/app/project/[id]/ideas/page.tsx | 7 +- src/app/project/[id]/layout.tsx | 7 +- src/components/IdeaBoard.tsx | 203 +++++++++++++++------ src/hooks/useProjects.ts | 65 +++++++ src/providers/ProjectProvider.tsx | 5 +- 9 files changed, 239 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index afcf0cb..b2026fa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* -*.next +.next/ .next/* node_modules @@ -26,82 +26,7 @@ dist-ssr *.sw? /src/generated/prisma -.next/dev/prerender-manifest.json +.next/dev/* .next/dev/trace .next/dev/cache/turbopack/23c46498/CURRENT .next/dev/cache/turbopack/23c46498/LOG -.next/dev/server/app-paths-manifest.json -.next/dev/server/next-font-manifest.js -.next/dev/server/next-font-manifest.json -.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js -.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js.map -.next/dev/server/chunks/\[root-of-the-server]__630db118._.js -.next/dev/server/chunks/\[root-of-the-server]__630db118._.js.map -.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js -.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__661e4e50._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js.map -.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css -.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js -.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js -.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js -.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_src_74b79b3f._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css -.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css.map -.next/dev/types/routes.d.ts -.next/dev/types/validator.ts -.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js -.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js.map -.next/dev/server/chunks/\[root-of-the-server]__630db118._.js -.next/dev/server/chunks/\[root-of-the-server]__630db118._.js.map -.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js -.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__661e4e50._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js.map -.next/dev/prerender-manifest.json -.next/dev/trace -.next/dev/cache/turbopack/23c46498/CURRENT -.next/dev/cache/turbopack/23c46498/LOG -.next/dev/server/app-paths-manifest.json -.next/dev/server/next-font-manifest.js -.next/dev/server/next-font-manifest.json -.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js -.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js.map -.next/dev/server/chunks/\[root-of-the-server]__630db118._.js -.next/dev/server/chunks/\[root-of-the-server]__630db118._.js.map -.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js -.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__661e4e50._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js.map -.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js -.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js.map -.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css -.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js -.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js -.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js -.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_src_74b79b3f._.js.map -.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css -.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css.map -.next/dev/types/routes.d.ts -.next/dev/types/validator.ts diff --git a/.next/dev/cache/turbopack/23c46498/CURRENT b/.next/dev/cache/turbopack/23c46498/CURRENT index 7dcc6041421bedee51431cce01c1bcd6d6bfbe8a..1ce5e25157783c36c45a374c18f2af53baa627ee 100644 GIT binary patch literal 4 LcmZQzFk1`&0TTe) literal 4 LcmZQzFkJ`$0S^Gz diff --git a/.next/dev/cache/turbopack/23c46498/LOG b/.next/dev/cache/turbopack/23c46498/LOG index f2beeb9..d8d36d1 100644 --- a/.next/dev/cache/turbopack/23c46498/LOG +++ b/.next/dev/cache/turbopack/23c46498/LOG @@ -6110,3 +6110,9 @@ FAM | META SEQ | SST SEQ | RANGE 0 | 00013727 | 00013726 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) 2 | 00013728 | 00013724 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh) 1 | 00013729 | 00013725 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh) +Time 2026-03-05T11:47:26.3903621Z +Commit 00013987 4 keys in 9ms 216µs 300ns +FAM | META SEQ | SST SEQ | RANGE + 0 | 00013985 | 00013984 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) + 1 | 00013986 | 00013982 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) + 2 | 00013987 | 00013983 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/project/[id]/ideas/page.tsx b/src/app/project/[id]/ideas/page.tsx index 6f273b2..70a3df6 100644 --- a/src/app/project/[id]/ideas/page.tsx +++ b/src/app/project/[id]/ideas/page.tsx @@ -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 ( updateProject({ ideas })} + onCreate={(data) => createIdea(projectId, data)} + onUpdateIdea={(id, data) => updateIdea(projectId, id, data)} + onDelete={(id) => deleteIdea(projectId, id)} /> ); } diff --git a/src/app/project/[id]/layout.tsx b/src/app/project/[id]/layout.tsx index 055bd8b..970b0ac 100644 --- a/src/app/project/[id]/layout.tsx +++ b/src/app/project/[id]/layout.tsx @@ -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, }}> diff --git a/src/components/IdeaBoard.tsx b/src/components/IdeaBoard.tsx index bb8ca6b..a9e8283 100644 --- a/src/components/IdeaBoard.tsx +++ b/src/components/IdeaBoard.tsx @@ -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) => Promise; + onUpdateIdea: (id: string, data: Partial) => Promise; + onDelete: (id: string) => Promise; } const CATEGORIES: Record = { @@ -26,10 +29,26 @@ const STATUS_LABELS: Record = { const MAX_DESCRIPTION_LENGTH = 500; -const IdeaBoard: React.FC = ({ ideas, onUpdate }) => { +const IdeaBoard: React.FC = ({ projectId, ideas, onCreate, onUpdateIdea, onDelete }) => { const { t } = useLanguage(); const [newIdeaTitle, setNewIdeaTitle] = useState(''); const [newIdeaCategory, setNewIdeaCategory] = useState('plot'); + const [localIdeas, setLocalIdeas] = useState(ideas); + + // Auto-Save State + const [saveStatus, setSaveStatus] = useState<'saved_local' | 'saved_db' | 'saving' | 'unsaved'>('saved_db'); + const saveTimeoutRef = useRef(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(null); @@ -39,12 +58,12 @@ const IdeaBoard: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 (
= ({ ideas, onUpdate }) => { {/* Header & Add Form (Top Bar) */}
-
-

- {t('ideaboard.title')} -

-

{t('ideaboard.desc')}

+
+
+

+ {t('ideaboard.title')} +

+

{t('ideaboard.desc')}

+
+ {/* Status Indicator for Mobile */} +
+ {saveStatus === 'saving' && <>} + {saveStatus === 'saved_local' && <>} + {saveStatus === 'saved_db' && <>} +
-
- - 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" - /> - -
+
+ {/* Status Indicator for Desktop */} +
+ {saveStatus === 'saving' && <> Sauvegarde...} + {saveStatus === 'saved_local' && <> Brouillon local} + {saveStatus === 'saved_db' && <> Sauvegardé} + {saveStatus === 'unsaved' && Non sauvegardé...} +
+ +
+ + 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" + /> + +
+
{/* Kanban Board */} diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index 91ecac2..77c0680 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -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) => { + 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) => { + 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 diff --git a/src/providers/ProjectProvider.tsx b/src/providers/ProjectProvider.tsx index 1235129..13d76af 100644 --- a/src/providers/ProjectProvider.tsx +++ b/src/providers/ProjectProvider.tsx @@ -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) => Promise; updateEntity: (entityId: string, data: Partial) => Promise; deleteEntity: (entityId: string) => Promise; + createIdea: (projectId: string, data: Partial) => Promise; + updateIdea: (projectId: string, ideaId: string, data: Partial) => Promise; + deleteIdea: (projectId: string, ideaId: string) => Promise; deleteProject: () => Promise; incrementUsage: () => void; }