diff --git a/.next/dev/cache/turbopack/23c46498/CURRENT b/.next/dev/cache/turbopack/23c46498/CURRENT
index 1ce5e25..187cfc9 100644
Binary files a/.next/dev/cache/turbopack/23c46498/CURRENT and b/.next/dev/cache/turbopack/23c46498/CURRENT differ
diff --git a/.next/dev/cache/turbopack/23c46498/LOG b/.next/dev/cache/turbopack/23c46498/LOG
index d8d36d1..5538d19 100644
--- a/.next/dev/cache/turbopack/23c46498/LOG
+++ b/.next/dev/cache/turbopack/23c46498/LOG
@@ -6116,3 +6116,9 @@ 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)
+Time 2026-03-05T17:21:32.7902721Z
+Commit 00014935 4 keys in 16ms 322µs 500ns
+FAM | META SEQ | SST SEQ | RANGE
+ 0 | 00014933 | 00014932 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
+ 1 | 00014934 | 00014930 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
+ 2 | 00014935 | 00014931 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/api/projects/[id]/chapters/reorder/route.ts b/src/app/api/projects/[id]/chapters/reorder/route.ts
new file mode 100644
index 0000000..8dd179b
--- /dev/null
+++ b/src/app/api/projects/[id]/chapters/reorder/route.ts
@@ -0,0 +1,49 @@
+export const dynamic = 'force-dynamic';
+
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
+ }
+
+ const { id: projectId } = await params;
+ const body = await request.json();
+ const { chapters } = body as { chapters: { id: string, orderIndex: number }[] };
+
+ if (!Array.isArray(chapters)) {
+ return NextResponse.json({ error: 'Format invalide' }, { status: 400 });
+ }
+
+ // Verify ownership
+ const existing = await prisma.project.findFirst({
+ where: { id: projectId, userId: session.user.id },
+ });
+
+ if (!existing) {
+ return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
+ }
+
+ try {
+ // Bulk update chapters' orderIndex within a transaction
+ await prisma.$transaction(
+ chapters.map((chapter) =>
+ prisma.chapter.update({
+ where: { id: chapter.id, projectId },
+ data: { orderIndex: chapter.orderIndex },
+ })
+ )
+ );
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Erreur lors de la réorganisation des chapitres:', error);
+ return NextResponse.json({ error: 'Erreur interne' }, { status: 500 });
+ }
+}
diff --git a/src/app/project/[id]/layout.tsx b/src/app/project/[id]/layout.tsx
index 970b0ac..4ac8741 100644
--- a/src/app/project/[id]/layout.tsx
+++ b/src/app/project/[id]/layout.tsx
@@ -33,7 +33,8 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
updateProject, updateChapter, addChapter,
createEntity, updateEntity, deleteEntity,
createIdea, updateIdea, deleteIdea,
- deleteProject
+ deleteProject,
+ reorderChapters
} = useProjects(user);
const { chatHistory, isGenerating, sendMessage } = useChat();
@@ -79,6 +80,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
);
}
+ // Handle early returns for non-existent projects
if (!project) {
return (
@@ -129,6 +131,8 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
onViewModeChange={handleViewModeChange}
onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }}
onUpdateProject={(updates) => updateProject(projectId, updates)}
+ onUpdateChapter={(chapterId, data) => updateChapter(projectId, chapterId, data)}
+ onReorderChapters={(chapters) => reorderChapters(projectId, chapters)}
onAddChapter={async () => {
const id = await addChapter(projectId, {});
if (id) {
diff --git a/src/components/AppRouter.tsx b/src/components/AppRouter.tsx
index 67c0f32..37438bd 100644
--- a/src/components/AppRouter.tsx
+++ b/src/components/AppRouter.tsx
@@ -43,8 +43,8 @@ interface AppRouterProps {
onUpdateProfile: (updates: Partial
) => void;
onUpgradePlan: (plan: any) => void;
onSendMessage: (msg: string) => void;
- onIncrementUsage: () => void;
onDeleteProject: (projectId: string) => void;
+ onReorderChapters: (projectId: string, chapters: { id: string, orderIndex: number }[]) => void;
}
const AppRouter: React.FC = (props) => {
@@ -139,6 +139,7 @@ const AppRouter: React.FC = (props) => {
onViewModeChange={props.onViewModeChange}
onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }}
onUpdateProject={props.onUpdateProject}
+ onReorderChapters={(chapters) => props.onReorderChapters(project.id, chapters)}
onAddChapter={async () => {
console.log("[AppRouter] onAddChapter triggered");
const id = await props.onAddChapter();
diff --git a/src/components/layout/EditorShell.tsx b/src/components/layout/EditorShell.tsx
index dff8c2d..d2b7c35 100644
--- a/src/components/layout/EditorShell.tsx
+++ b/src/components/layout/EditorShell.tsx
@@ -1,8 +1,8 @@
'use client';
-import React, { useState, useEffect } from 'react';
-import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types';
+import React, { useState, useEffect, useRef } from 'react';
+import { BookProject, UserProfile, ViewMode, ChatMessage, Chapter } from '@/lib/types';
import AIPanel from '@/components/AIPanel';
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2, Wand2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
@@ -18,6 +18,8 @@ interface EditorShellProps {
onViewModeChange: (mode: ViewMode) => void;
onChapterSelect: (id: string) => void;
onUpdateProject: (updates: Partial) => void;
+ onUpdateChapter?: (chapterId: string, updates: Partial) => void;
+ onReorderChapters?: (chapters: { id: string, orderIndex: number }[]) => void;
onAddChapter: () => Promise;
onDeleteChapter: (id: string) => void;
onLogout: () => void;
@@ -32,10 +34,28 @@ const EditorShell: React.FC = (props) => {
const { project, user, viewMode, currentChapterId, children } = props;
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
+ const [draggedChapterIdx, setDraggedChapterIdx] = useState(null);
+ const [dragOverChapterIdx, setDragOverChapterIdx] = useState(null);
const { t } = useLanguage();
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
+ // Local state for debounced title input
+ const [localTitle, setLocalTitle] = useState(currentChapter?.title || "");
+
+ useEffect(() => {
+ // Only update local title from props if it differs from what's currently in state,
+ // to prevent overwriting while the user is typing, but still catch external updates (e.g. from DB load)
+ if (currentChapter?.title && currentChapter.title !== localTitle) {
+ setLocalTitle(currentChapter.title);
+ } else if (!currentChapter) {
+ setLocalTitle("");
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentChapter?.title, currentChapterId]);
+
+ const titleDebounceRef = useRef(null);
+
// Auto-close sidebars on mobile when navigating
useEffect(() => {
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
@@ -81,14 +101,63 @@ const EditorShell: React.FC = (props) => {
{t('nav.chapters')}
{project.chapters.map((chap, idx) => (
-
+
idx
+ ? 'border-t-blue-500 border-b-transparent bg-blue-900/20'
+ : 'border-b-blue-500 border-t-transparent bg-blue-900/20'
+ ) : 'border-transparent'
+ }`}
+ draggable
+ onDragStart={(e) => {
+ setDraggedChapterIdx(idx);
+ e.dataTransfer.effectAllowed = 'move';
+ }}
+ onDragOver={(e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ }}
+ onDragEnter={() => {
+ if (draggedChapterIdx !== null && draggedChapterIdx !== idx) {
+ setDragOverChapterIdx(idx);
+ }
+ }}
+ onDragLeave={() => {
+ // optional edge smoothing here
+ }}
+ onDragEnd={() => {
+ setDraggedChapterIdx(null);
+ setDragOverChapterIdx(null);
+ }}
+ onDrop={(e) => {
+ e.preventDefault();
+ if (draggedChapterIdx === null || draggedChapterIdx === idx) return;
+
+ const newChapters = [...project.chapters];
+ const [removed] = newChapters.splice(draggedChapterIdx, 1);
+ newChapters.splice(idx, 0, removed);
+
+ setDraggedChapterIdx(null);
+ setDragOverChapterIdx(null);
+
+ // Re-index chapters to match new visual order
+ const updatedChapters = newChapters.map((c, i) => ({ id: c.id, orderIndex: i }));
+ if (props.onReorderChapters) {
+ props.onReorderChapters(updatedChapters);
+ } else {
+ props.onUpdateProject({ chapters: newChapters.map((c, i) => ({ ...c, orderIndex: i })) });
+ }
+ }}
+ >
-
+
))}
@@ -123,8 +192,28 @@ const EditorShell: React.FC
= (props) => {
{viewMode === 'write' ? (
props.onUpdateProject({ chapters: project.chapters.map(c => c.id === currentChapterId ? { ...c, title: e.target.value } : c) })}
+ value={localTitle}
+ onChange={(e) => {
+ const newTitle = e.target.value;
+ setLocalTitle(newTitle);
+
+ if (currentChapterId.startsWith('placeholder-')) return;
+
+ // Local optimistic update via project for UI consistency
+ props.onUpdateProject({
+ chapters: project.chapters.map(c =>
+ c.id === currentChapterId ? { ...c, title: newTitle } : c
+ )
+ });
+
+ // Debounce the actual backend save
+ if (titleDebounceRef.current) clearTimeout(titleDebounceRef.current);
+ titleDebounceRef.current = setTimeout(() => {
+ if (props.onUpdateChapter) {
+ props.onUpdateChapter(currentChapterId, { title: newTitle });
+ }
+ }, 1000);
+ }}
className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none"
/>
) : (
diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts
index 77c0680..5c1b98a 100644
--- a/src/hooks/useProjects.ts
+++ b/src/hooks/useProjects.ts
@@ -213,6 +213,8 @@ export const useProjects = (user: UserProfile | null) => {
};
const updateChapter = async (projectId: string, chapterId: string, data: Partial) => {
+ if (chapterId.startsWith('placeholder-')) return;
+
setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p;
return {
@@ -228,6 +230,34 @@ export const useProjects = (user: UserProfile | null) => {
}
};
+ const reorderChapters = async (projectId: string, chaptersToReorder: { id: string, orderIndex: number }[]) => {
+ // Optimistic UI update
+ setProjects(prev => prev.map(p => {
+ if (p.id !== projectId) return p;
+
+ // Map the new order indexes to the existing chapters
+ const orderMap = new Map(chaptersToReorder.map(c => [c.id, c.orderIndex]));
+ const updatedChapters = p.chapters.map(c => {
+ if (orderMap.has(c.id)) {
+ return { ...c, orderIndex: orderMap.get(c.id)! };
+ }
+ return c;
+ });
+
+ // Sort them immediately so the UI doesn't jump on next render
+ updatedChapters.sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0));
+
+ return { ...p, chapters: updatedChapters };
+ }));
+
+ try {
+ await api.chapters.reorder(projectId, chaptersToReorder);
+ } catch (err) {
+ console.error("Failed to reorder chapters", err);
+ // In a real app we might revert the state here on failure
+ }
+ };
+
const createEntity = async (projectId: string, type: EntityType, initialData?: Partial) => {
try {
const newEntity = await api.entities.create({
@@ -363,6 +393,7 @@ export const useProjects = (user: UserProfile | null) => {
updateProject,
addChapter,
updateChapter,
+ reorderChapters,
createEntity,
updateEntity,
deleteEntity,
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 5d154db..76ec9e2 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -113,6 +113,13 @@ const api = {
method: 'DELETE',
});
},
+
+ async reorder(projectId: string, chapters: { id: string, orderIndex: number }[]) {
+ return api.request(`/projects/${projectId}/chapters/reorder`, {
+ method: 'PUT',
+ body: JSON.stringify({ chapters }),
+ });
+ },
},
// --- ENTITIES ---
diff --git a/src/lib/types.ts b/src/lib/types.ts
index e8e0200..896742f 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -53,6 +53,7 @@ export interface Chapter {
title: string;
content: string;
summary?: string;
+ orderIndex?: number;
}
export type PlotNodeType = 'story' | 'dialogue' | 'action';