200 lines
9.0 KiB
TypeScript
200 lines
9.0 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { Document, docApi } from '@/lib/api-docs';
|
|
import {
|
|
XMarkIcon,
|
|
CheckIcon,
|
|
ArrowLeftIcon,
|
|
Bars3BottomLeftIcon,
|
|
HashtagIcon,
|
|
CloudArrowUpIcon,
|
|
SparklesIcon
|
|
} from "@heroicons/react/24/outline";
|
|
import NotionEditor from './NotionEditor';
|
|
import DocumentSidebar from './DocumentSidebar';
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
interface DocumentEditorProps {
|
|
initialDocument: Partial<Document> | null;
|
|
onSave: (doc: Partial<Document>) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export default function DocumentEditor({ initialDocument, onSave, onCancel }: DocumentEditorProps) {
|
|
const [document, setDocument] = useState<Partial<Document> | null>(initialDocument);
|
|
const [title, setTitle] = useState(initialDocument?.title || '');
|
|
const [content, setContent] = useState(initialDocument?.content || '[]');
|
|
const [showSidebar, setShowSidebar] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// Refs para controle fino de salvamento
|
|
const saveTimeout = useRef<NodeJS.Timeout | null>(null);
|
|
const lastSaved = useRef({ title: initialDocument?.title || '', content: initialDocument?.content || '[]' });
|
|
|
|
useEffect(() => {
|
|
if (initialDocument) {
|
|
setDocument(initialDocument);
|
|
setTitle(initialDocument.title || '');
|
|
setContent(initialDocument.content || '[]');
|
|
lastSaved.current = {
|
|
title: initialDocument.title || '',
|
|
content: initialDocument.content || '[]'
|
|
};
|
|
}
|
|
}, [initialDocument]);
|
|
|
|
// Função de Auto-Save Robusta
|
|
const autoSave = useCallback(async (newTitle: string, newContent: string) => {
|
|
if (!document?.id) return;
|
|
|
|
setSaving(true);
|
|
console.log('💾 Inactivity detected. Saving document...', document.id);
|
|
|
|
try {
|
|
await docApi.updateDocument(document.id, {
|
|
title: newTitle,
|
|
content: newContent,
|
|
status: 'published'
|
|
});
|
|
// Atualiza o ref do último salvo para evitar loop
|
|
lastSaved.current = { title: newTitle, content: newContent };
|
|
console.log('✅ Document saved successfully');
|
|
} catch (e) {
|
|
console.error('❌ Auto-save failed', e);
|
|
toast.error('Erro ao salvar automaticamente');
|
|
} finally {
|
|
// Delay visual para o feedback de "Salvo"
|
|
setTimeout(() => setSaving(false), 800);
|
|
}
|
|
}, [document?.id]);
|
|
|
|
// Trigger de auto-save com debounce de 1 segundo
|
|
useEffect(() => {
|
|
if (!document?.id) return;
|
|
|
|
// Verifica se houve mudança real em relação ao último salvo
|
|
if (title === lastSaved.current.title && content === lastSaved.current.content) {
|
|
return;
|
|
}
|
|
|
|
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
|
|
saveTimeout.current = setTimeout(() => {
|
|
autoSave(title, content);
|
|
}, 1000); // Salva após 1 segundo de inatividade
|
|
|
|
return () => {
|
|
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
};
|
|
}, [title, content, document?.id, autoSave]);
|
|
|
|
const navigateToDoc = async (doc: Document) => {
|
|
// Antes de navegar, salva o atual se necessário
|
|
if (title !== lastSaved.current.title || content !== lastSaved.current.content) {
|
|
await autoSave(title, content);
|
|
}
|
|
|
|
setDocument(doc);
|
|
setTitle(doc.title);
|
|
setContent(doc.content);
|
|
lastSaved.current = { title: doc.title, content: doc.content };
|
|
toast.success(`Abrindo: ${doc.title || 'Untitled'}`, { duration: 1000, position: 'bottom-center' });
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[60] bg-white dark:bg-zinc-950 flex flex-col">
|
|
{/* Header Clean */}
|
|
<header className="h-16 border-b border-zinc-200 dark:border-zinc-800 px-6 flex items-center justify-between bg-white dark:bg-zinc-950 z-20 shrink-0">
|
|
<div className="flex items-center gap-4 flex-1">
|
|
<button
|
|
onClick={async () => {
|
|
if (title !== lastSaved.current.title || content !== lastSaved.current.content) {
|
|
await autoSave(title, content);
|
|
}
|
|
onCancel();
|
|
}}
|
|
className="p-2 text-zinc-400 hover:text-zinc-900 dark:hover:text-white rounded-xl hover:bg-zinc-100 dark:hover:bg-zinc-900 transition-all"
|
|
title="Back to list"
|
|
>
|
|
<ArrowLeftIcon className="w-5 h-5" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setShowSidebar(!showSidebar)}
|
|
className={`p-2 rounded-xl transition-all ${showSidebar ? 'text-brand-500 bg-brand-50/50 dark:bg-brand-500/10' : 'text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-900'}`}
|
|
title={showSidebar ? "Hide Navigation" : "Show Navigation"}
|
|
>
|
|
<Bars3BottomLeftIcon className="w-5 h-5" />
|
|
</button>
|
|
|
|
<div className="h-4 w-px bg-zinc-200 dark:bg-zinc-800 mx-2" />
|
|
|
|
<div className="flex items-center gap-2 flex-1">
|
|
<HashtagIcon className="w-4 h-4 text-zinc-300" />
|
|
<input
|
|
type="text"
|
|
placeholder="Untitled Document"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
className="text-lg font-bold bg-transparent border-none outline-none text-zinc-900 dark:text-white placeholder:text-zinc-300 dark:placeholder:text-zinc-600 w-full max-w-xl"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-50 dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800">
|
|
{saving ? (
|
|
<div className="flex items-center gap-2 text-brand-500">
|
|
<div className="w-1.5 h-1.5 bg-brand-500 rounded-full animate-pulse" />
|
|
<span className="text-[10px] font-black uppercase tracking-widest leading-none">Saving...</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-emerald-500">
|
|
<CheckIcon className="w-3.5 h-3.5" />
|
|
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-500 dark:text-zinc-400 leading-none">Sync Saved</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Lateral Sidebar (Navegação) */}
|
|
{showSidebar && document?.id && (
|
|
<DocumentSidebar
|
|
key={document.id} // Re-render sidebar on doc change to ensure fresh data
|
|
documentId={document.id}
|
|
onNavigate={navigateToDoc}
|
|
/>
|
|
)}
|
|
|
|
{/* Área do Editor */}
|
|
<main className="flex-1 overflow-y-auto bg-white dark:bg-zinc-950 flex flex-col items-center custom-scrollbar">
|
|
<div className="w-full max-w-[850px] px-12 md:px-24 py-16 animate-in slide-in-from-bottom-2 duration-500">
|
|
{/* Title Hero Display */}
|
|
<div className="mb-14 group">
|
|
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter leading-tight">
|
|
{title || 'Untitled'}
|
|
</h1>
|
|
<div className="mt-6 flex items-center gap-4 text-zinc-400">
|
|
<SparklesIcon className="w-4 h-4 text-brand-500" />
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-5 h-5 rounded-full bg-brand-500 flex items-center justify-center text-[10px] font-bold text-white uppercase">Me</div>
|
|
<span className="text-[10px] font-black uppercase tracking-widest">Editing Mode • Real-time Sync</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<NotionEditor
|
|
documentId={document?.id}
|
|
initialContent={content}
|
|
onChange={setContent}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|