petit responsive ++ correction editeur de texte
This commit is contained in:
@@ -2,8 +2,7 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
// 1. On remplace l'import de getDB par l'objet prisma direct
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getDB } from '@/lib/prisma';
|
||||
|
||||
// PUT /api/chapters/[id] — Update a chapter
|
||||
export async function PUT(
|
||||
@@ -18,8 +17,8 @@ export async function PUT(
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// 2. On utilise 'prisma' au lieu de 'getDB()'
|
||||
const chapter = await prisma.chapter.findUnique({
|
||||
const db = getDB();
|
||||
const chapter = await db.chapter.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
@@ -27,7 +26,7 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updated = await prisma.chapter.update({
|
||||
const updated = await db.chapter.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.title !== undefined && { title: body.title }),
|
||||
@@ -52,8 +51,8 @@ export async function DELETE(
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// 3. On utilise 'prisma' au lieu de 'getDB()'
|
||||
const chapter = await prisma.chapter.findUnique({
|
||||
const db = getDB();
|
||||
const chapter = await db.chapter.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
@@ -61,7 +60,7 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
await prisma.chapter.delete({ where: { id } });
|
||||
await db.chapter.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
import { ProjectProvider } from '@/providers/ProjectProvider';
|
||||
@@ -27,6 +27,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
||||
const projectId = params.id as string;
|
||||
|
||||
const { user, logout, incrementUsage, loading: authLoading } = useAuthContext();
|
||||
const hasEverLoaded = useRef(false);
|
||||
const {
|
||||
projects, setCurrentProjectId,
|
||||
updateProject, updateChapter, addChapter,
|
||||
@@ -40,6 +41,13 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
||||
|
||||
const viewMode = getViewModeFromPath(pathname);
|
||||
|
||||
// Track when auth has loaded at least once to avoid unmounting on session refresh
|
||||
useEffect(() => {
|
||||
if (!authLoading && user) {
|
||||
hasEverLoaded.current = true;
|
||||
}
|
||||
}, [authLoading, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) setCurrentProjectId(projectId);
|
||||
}, [projectId, setCurrentProjectId]);
|
||||
@@ -56,7 +64,8 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
|
||||
}
|
||||
}, [project, currentChapterId]);
|
||||
|
||||
if (authLoading || !user) {
|
||||
// Only show loading spinner on INITIAL load, not during session refreshes (tab switch)
|
||||
if (!hasEverLoaded.current && (authLoading || !user)) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
|
||||
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
|
||||
|
||||
@@ -28,7 +28,13 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
|
||||
<div className="flex flex-col md:flex-row justify-between items-center bg-theme-panel p-6 md:p-8 rounded-[2rem] shadow-sm border border-theme-border gap-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="relative">
|
||||
<img src={user.avatar} className="w-20 h-20 rounded-full border-4 border-slate-50 shadow-lg object-cover" alt="Avatar" />
|
||||
{user.avatar ? (
|
||||
<img src={user.avatar} className="w-20 h-20 rounded-full border-4 border-slate-50 shadow-lg object-cover" alt="Avatar" />
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-full border-4 border-slate-50 shadow-lg bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white text-2xl font-black">
|
||||
{user.name?.charAt(0)?.toUpperCase() || '?'}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -bottom-1 -right-1 bg-green-500 w-5 h-5 rounded-full border-4 border-white" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -98,7 +104,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
|
||||
<Book size={24} />
|
||||
</div>
|
||||
<h4 className="font-black text-theme-text text-xl truncate mb-1">{p.title}</h4>
|
||||
<p className="text-theme-muted text-sm">{t('dashboard.last_modified')} : {new Date(p.lastModified).toLocaleDateString()}</p>
|
||||
<p className="text-theme-muted text-sm">{t('dashboard.last_modified')} : {new Date(p.lastModified).toLocaleDateString('fr-FR')}</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-[10px] text-theme-muted font-black uppercase tracking-widest border-t border-theme-border pt-4 mt-auto">
|
||||
<span>{p.chapters.length} {t('nav.chapters')}</span>
|
||||
|
||||
@@ -204,7 +204,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
|
||||
|
||||
<div className="flex justify-between items-center text-xs text-theme-muted border-t border-theme-border pt-2 mt-2 transition-colors duration-300">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={10} /> {new Date(idea.createdAt).toLocaleDateString()}
|
||||
<Clock size={10} /> {new Date(idea.createdAt).toLocaleDateString('fr-FR')}
|
||||
</span>
|
||||
<GripVertical size={14} className="opacity-20" />
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,11 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved');
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Track sync state to avoid autosave loopbacks wiping current edits
|
||||
// Start as null so the initial useEffect ALWAYS writes initialContent to the div
|
||||
const syncRef = useRef<string | null>(null);
|
||||
const latestContentRef = useRef<string>(initialContent);
|
||||
|
||||
// Context Menu State
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
@@ -161,14 +166,33 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
// --- Effects ---
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current && contentRef.current.innerHTML !== initialContent) {
|
||||
// Only update if difference is significant to avoid cursor jumps on small re-renders?
|
||||
// OR better: Only update if NOT focused?
|
||||
if (!isFocused && Math.abs(contentRef.current.innerHTML.length - initialContent.length) > 5) {
|
||||
contentRef.current.innerHTML = initialContent;
|
||||
if (!contentRef.current || initialContent === undefined) return;
|
||||
|
||||
// Ignore exact loopbacks from our own saves
|
||||
if (initialContent === syncRef.current) return;
|
||||
|
||||
// Safety: never overwrite real content with an empty string from a stale/placeholder source
|
||||
const hasRealContent = latestContentRef.current && latestContentRef.current.trim().length > 0;
|
||||
if (!initialContent && hasRealContent) return;
|
||||
|
||||
// We reached here, so initialContent is genuinely NEW data we didn't know about.
|
||||
// E.g. clicked another chapter, or data was modified in another tab/device.
|
||||
contentRef.current.innerHTML = initialContent;
|
||||
syncRef.current = initialContent;
|
||||
latestContentRef.current = initialContent;
|
||||
}, [initialContent]);
|
||||
|
||||
// Flush pending save on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
if (latestContentRef.current !== syncRef.current && onSave) {
|
||||
onSave(latestContentRef.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [initialContent, isFocused]);
|
||||
};
|
||||
}, [onSave]);
|
||||
|
||||
// --- Event Handlers ---
|
||||
|
||||
@@ -180,7 +204,10 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
|
||||
const handleInput = () => {
|
||||
if (contentRef.current) {
|
||||
if (onChange) onChange(contentRef.current.innerHTML);
|
||||
const currentHtml = contentRef.current.innerHTML;
|
||||
latestContentRef.current = currentHtml;
|
||||
|
||||
if (onChange) onChange(currentHtml);
|
||||
|
||||
// Auto-Save Debounce
|
||||
if (onSave) {
|
||||
@@ -189,7 +216,9 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
|
||||
saveTimeoutRef.current = setTimeout(async () => {
|
||||
setSaveStatus('saving');
|
||||
await onSave(contentRef.current?.innerHTML || "");
|
||||
const htmlToSave = latestContentRef.current;
|
||||
await onSave(htmlToSave);
|
||||
syncRef.current = htmlToSave; // Record that we've synced this exact string to the server
|
||||
setSaveStatus('saved');
|
||||
}, 2000); // 2 seconds
|
||||
}
|
||||
@@ -370,6 +399,8 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
ref={contentRef}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={true}
|
||||
lang="fr-FR"
|
||||
className="bg-theme-editor-bg shadow-sm w-[800px] min-h-[1000px] p-12 outline-none font-serif text-lg leading-relaxed text-theme-editor-text editor-content transition-colors duration-300"
|
||||
onInput={handleInput}
|
||||
onBlur={() => { setIsFocused(false); saveSelection(); }}
|
||||
|
||||
@@ -20,9 +20,10 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Load Projects
|
||||
// Load Projects - only re-fetch when user ID actually changes, not on every user object reference change
|
||||
const userId = user?.id;
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
if (!userId) {
|
||||
setProjects([]);
|
||||
return;
|
||||
}
|
||||
@@ -41,7 +42,19 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
ideas: [],
|
||||
settings: p.settings || undefined
|
||||
}));
|
||||
setProjects(mapped);
|
||||
// Merge: keep existing fully-loaded projects, only add new ones from the list
|
||||
setProjects(prev => {
|
||||
const existingIds = new Set(prev.filter(p => !p.chapters.some(c => c.id.startsWith('placeholder-'))).map(p => p.id));
|
||||
const merged = prev.filter(p => existingIds.has(p.id));
|
||||
for (const mp of mapped) {
|
||||
if (!existingIds.has(mp.id)) {
|
||||
merged.push(mp);
|
||||
}
|
||||
}
|
||||
// Remove projects no longer in the server list
|
||||
const serverIds = new Set(mapped.map(p => p.id));
|
||||
return merged.filter(p => serverIds.has(p.id));
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load projects', err);
|
||||
} finally {
|
||||
@@ -49,7 +62,7 @@ export const useProjects = (user: UserProfile | null) => {
|
||||
}
|
||||
};
|
||||
loadProjects();
|
||||
}, [user]);
|
||||
}, [userId]);
|
||||
|
||||
// Load details when project is selected
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user