sauvegarde boite a idée
This commit is contained in:
79
.gitignore
vendored
79
.gitignore
vendored
@@ -6,7 +6,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
*.next
|
.next/
|
||||||
.next/*
|
.next/*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
@@ -26,82 +26,7 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
/src/generated/prisma
|
/src/generated/prisma
|
||||||
.next/dev/prerender-manifest.json
|
.next/dev/*
|
||||||
.next/dev/trace
|
.next/dev/trace
|
||||||
.next/dev/cache/turbopack/23c46498/CURRENT
|
.next/dev/cache/turbopack/23c46498/CURRENT
|
||||||
.next/dev/cache/turbopack/23c46498/LOG
|
.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
|
|
||||||
|
|||||||
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
Binary file not shown.
6
.next/dev/cache/turbopack/23c46498/LOG
vendored
6
.next/dev/cache/turbopack/23c46498/LOG
vendored
@@ -6110,3 +6110,9 @@ FAM | META SEQ | SST SEQ | RANGE
|
|||||||
0 | 00013727 | 00013726 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
|
0 | 00013727 | 00013726 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
|
||||||
2 | 00013728 | 00013724 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh)
|
2 | 00013728 | 00013724 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh)
|
||||||
1 | 00013729 | 00013725 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)
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import IdeaBoard from '@/components/IdeaBoard';
|
|||||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||||
|
|
||||||
export default function IdeasPage() {
|
export default function IdeasPage() {
|
||||||
const { project, updateProject } = useProjectContext();
|
const { project, projectId, createIdea, updateIdea, deleteIdea } = useProjectContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IdeaBoard
|
<IdeaBoard
|
||||||
|
projectId={projectId}
|
||||||
ideas={project.ideas || []}
|
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 {
|
const {
|
||||||
projects, setCurrentProjectId,
|
projects, setCurrentProjectId,
|
||||||
updateProject, updateChapter, addChapter,
|
updateProject, updateChapter, addChapter,
|
||||||
createEntity, updateEntity, deleteEntity, deleteProject
|
createEntity, updateEntity, deleteEntity,
|
||||||
|
createIdea, updateIdea, deleteIdea,
|
||||||
|
deleteProject
|
||||||
} = useProjects(user);
|
} = useProjects(user);
|
||||||
const { chatHistory, isGenerating, sendMessage } = useChat();
|
const { chatHistory, isGenerating, sendMessage } = useChat();
|
||||||
|
|
||||||
@@ -111,6 +113,9 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
|||||||
createEntity: (type, data) => createEntity(projectId, type, data),
|
createEntity: (type, data) => createEntity(projectId, type, data),
|
||||||
updateEntity: (entityId, data) => updateEntity(projectId, entityId, data),
|
updateEntity: (entityId, data) => updateEntity(projectId, entityId, data),
|
||||||
deleteEntity: (entityId) => deleteEntity(projectId, entityId),
|
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),
|
deleteProject: () => deleteProject(projectId),
|
||||||
incrementUsage,
|
incrementUsage,
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types';
|
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 { useLanguage } from '@/providers/LanguageProvider';
|
||||||
import { TranslationKey } from '@/lib/i18n/translations';
|
import { TranslationKey } from '@/lib/i18n/translations';
|
||||||
|
|
||||||
interface IdeaBoardProps {
|
interface IdeaBoardProps {
|
||||||
|
projectId: string;
|
||||||
ideas: Idea[];
|
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 }> = {
|
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 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 { t } = useLanguage();
|
||||||
const [newIdeaTitle, setNewIdeaTitle] = useState('');
|
const [newIdeaTitle, setNewIdeaTitle] = useState('');
|
||||||
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
|
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
|
// Drag and Drop State
|
||||||
const [draggedIdeaId, setDraggedIdeaId] = useState<string | null>(null);
|
const [draggedIdeaId, setDraggedIdeaId] = useState<string | null>(null);
|
||||||
@@ -39,12 +58,12 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
|||||||
|
|
||||||
// --- ACTIONS ---
|
// --- ACTIONS ---
|
||||||
|
|
||||||
const handleAddIdea = (e: React.FormEvent) => {
|
const handleAddIdea = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newIdeaTitle.trim()) return;
|
if (!newIdeaTitle.trim()) return;
|
||||||
|
|
||||||
const newIdea: Idea = {
|
const newIdea: Idea = {
|
||||||
id: `idea-${Date.now()}`,
|
id: `temp-${Date.now()}`,
|
||||||
title: newIdeaTitle,
|
title: newIdeaTitle,
|
||||||
description: '',
|
description: '',
|
||||||
category: newIdeaCategory,
|
category: newIdeaCategory,
|
||||||
@@ -52,36 +71,86 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
|||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
onUpdate([...ideas, newIdea]);
|
saveLocally([...localIdeas, newIdea]);
|
||||||
setNewIdeaTitle('');
|
setNewIdeaTitle('');
|
||||||
};
|
setSaveStatus('saving');
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
try {
|
||||||
if (confirm(t('ideaboard.delete') + " ?")) {
|
await onCreate({
|
||||||
onUpdate(ideas.filter(i => i.id !== id));
|
title: newIdea.title,
|
||||||
if (editingItem?.id === id) setEditingItem(null);
|
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 || !editingItem.title?.trim()) return;
|
||||||
|
|
||||||
if (editingItem.id) {
|
let isNew = !editingItem.id || editingItem.id.startsWith('temp-');
|
||||||
|
|
||||||
|
if (!isNew) {
|
||||||
// Update existing
|
// 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 {
|
} else {
|
||||||
// Create new from modal
|
// Create new from modal
|
||||||
const newIdea: Idea = {
|
const newIdea: Idea = {
|
||||||
id: `idea-${Date.now()}`,
|
id: `temp-${Date.now()}`,
|
||||||
title: editingItem.title || '',
|
title: editingItem.title || '',
|
||||||
description: editingItem.description || '',
|
description: editingItem.description || '',
|
||||||
category: editingItem.category || 'plot',
|
category: editingItem.category || 'plot',
|
||||||
status: editingItem.status || 'todo',
|
status: editingItem.status || 'todo',
|
||||||
createdAt: Date.now()
|
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) => {
|
const openQuickAdd = (status: IdeaStatus) => {
|
||||||
@@ -104,14 +173,24 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
|||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent, status: IdeaStatus) => {
|
const handleDrop = async (e: React.DragEvent, status: IdeaStatus) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (draggedIdeaId) {
|
if (draggedIdeaId) {
|
||||||
const updatedIdeas = ideas.map(idea =>
|
saveLocally(localIdeas.map(idea =>
|
||||||
idea.id === draggedIdeaId ? { ...idea, status } : idea
|
idea.id === draggedIdeaId ? { ...idea, status } : idea
|
||||||
);
|
));
|
||||||
onUpdate(updatedIdeas);
|
const idToUpdate = draggedIdeaId;
|
||||||
setDraggedIdeaId(null);
|
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 ---
|
// --- RENDERERS ---
|
||||||
|
|
||||||
const Column = ({ title, status, icon: Icon }: { title: string, status: IdeaStatus, icon: any }) => {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -227,38 +306,56 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
|||||||
|
|
||||||
{/* Header & Add Form (Top Bar) */}
|
{/* 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 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>
|
<div className="flex justify-between items-center w-full md:w-auto">
|
||||||
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2">
|
<div>
|
||||||
<Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
|
<h2 className="text-xl font-bold text-theme-text flex items-center gap-2">
|
||||||
</h2>
|
<Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
|
||||||
<p className="text-theme-muted text-sm">{t('ideaboard.desc')}</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-2xl flex gap-2">
|
<div className="flex items-center gap-4 w-full md:w-auto flex-1 justify-end">
|
||||||
<select
|
{/* Status Indicator for Desktop */}
|
||||||
value={newIdeaCategory}
|
<div className="hidden md:flex items-center gap-2 text-xs font-medium text-slate-400">
|
||||||
onChange={(e) => setNewIdeaCategory(e.target.value as IdeaCategory)}
|
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /> <span className="text-blue-500">Sauvegarde...</span></>}
|
||||||
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"
|
{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></>}
|
||||||
{Object.entries(CATEGORIES).map(([key, val]) => (
|
{saveStatus === 'unsaved' && <span className="text-amber-500">Non sauvegardé...</span>}
|
||||||
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
|
</div>
|
||||||
))}
|
|
||||||
</select>
|
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-lg flex gap-2">
|
||||||
<input
|
<select
|
||||||
type="text"
|
value={newIdeaCategory}
|
||||||
value={newIdeaTitle}
|
onChange={(e) => setNewIdeaCategory(e.target.value as IdeaCategory)}
|
||||||
onChange={(e) => setNewIdeaTitle(e.target.value)}
|
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"
|
||||||
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"
|
{Object.entries(CATEGORIES).map(([key, val]) => (
|
||||||
/>
|
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
|
||||||
<button
|
))}
|
||||||
type="submit"
|
</select>
|
||||||
disabled={!newIdeaTitle.trim()}
|
<input
|
||||||
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"
|
type="text"
|
||||||
>
|
value={newIdeaTitle}
|
||||||
<Plus size={18} />
|
onChange={(e) => setNewIdeaTitle(e.target.value)}
|
||||||
</button>
|
placeholder={t('ideaboard.add_idea')}
|
||||||
</form>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Kanban Board */}
|
{/* Kanban Board */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
EntityType,
|
EntityType,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
Idea
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import {
|
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 {
|
return {
|
||||||
projects,
|
projects,
|
||||||
currentProjectId,
|
currentProjectId,
|
||||||
@@ -304,6 +366,9 @@ export const useProjects = (user: UserProfile | null) => {
|
|||||||
createEntity,
|
createEntity,
|
||||||
updateEntity,
|
updateEntity,
|
||||||
deleteEntity,
|
deleteEntity,
|
||||||
|
createIdea,
|
||||||
|
updateIdea,
|
||||||
|
deleteIdea,
|
||||||
deleteProject: async (projectId: string) => {
|
deleteProject: async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
// Cascade delete is handled by Prisma, just delete the project
|
// Cascade delete is handled by Prisma, just delete the project
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext } from 'react';
|
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 {
|
interface ProjectContextType {
|
||||||
project: BookProject;
|
project: BookProject;
|
||||||
@@ -14,6 +14,9 @@ interface ProjectContextType {
|
|||||||
createEntity: (type: EntityType, initialData?: Partial<Entity>) => Promise<string>;
|
createEntity: (type: EntityType, initialData?: Partial<Entity>) => Promise<string>;
|
||||||
updateEntity: (entityId: string, data: Partial<Entity>) => Promise<void>;
|
updateEntity: (entityId: string, data: Partial<Entity>) => Promise<void>;
|
||||||
deleteEntity: (entityId: string) => 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>;
|
deleteProject: () => Promise<void>;
|
||||||
incrementUsage: () => void;
|
incrementUsage: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user