petit responsive ++ correction editeur de texte

This commit is contained in:
2026-03-04 22:01:36 +01:00
parent c8fffece3e
commit 5b1bd74d9c
365 changed files with 6373 additions and 2514 deletions

View File

@@ -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 });
}

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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(); }}

View File

@@ -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(() => {