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';