commit before hard reset to change connection
This commit is contained in:
@@ -118,8 +118,13 @@ const AppRouter: React.FC<AppRouterProps> = (props) => {
|
|||||||
onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }}
|
onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }}
|
||||||
onUpdateProject={props.onUpdateProject}
|
onUpdateProject={props.onUpdateProject}
|
||||||
onAddChapter={async () => {
|
onAddChapter={async () => {
|
||||||
|
console.log("[AppRouter] onAddChapter triggered");
|
||||||
const id = await props.onAddChapter();
|
const id = await props.onAddChapter();
|
||||||
if (id) setCurrentChapterId(id);
|
console.log("[AppRouter] onAddChapter result ID:", id);
|
||||||
|
if (id) {
|
||||||
|
setCurrentChapterId(id);
|
||||||
|
props.onViewModeChange('write');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onDeleteChapter={(id) => {
|
onDeleteChapter={(id) => {
|
||||||
if (project.chapters.length > 1) {
|
if (project.chapters.length > 1) {
|
||||||
|
|||||||
@@ -5,128 +5,128 @@ import AIPanel from '../AIPanel';
|
|||||||
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2 } from 'lucide-react';
|
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
interface EditorShellProps {
|
interface EditorShellProps {
|
||||||
project: BookProject;
|
project: BookProject;
|
||||||
user: UserProfile;
|
user: UserProfile;
|
||||||
viewMode: ViewMode;
|
viewMode: ViewMode;
|
||||||
currentChapterId: string;
|
currentChapterId: string;
|
||||||
chatHistory: ChatMessage[];
|
chatHistory: ChatMessage[];
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
onViewModeChange: (mode: ViewMode) => void;
|
onViewModeChange: (mode: ViewMode) => void;
|
||||||
onChapterSelect: (id: string) => void;
|
onChapterSelect: (id: string) => void;
|
||||||
onUpdateProject: (updates: Partial<BookProject>) => void;
|
onUpdateProject: (updates: Partial<BookProject>) => void;
|
||||||
onAddChapter: () => void;
|
onAddChapter: () => Promise<void>;
|
||||||
onDeleteChapter: (id: string) => void;
|
onDeleteChapter: (id: string) => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
onSendMessage: (msg: string) => void;
|
onSendMessage: (msg: string) => void;
|
||||||
onInsertText: (text: string) => void;
|
onInsertText: (text: string) => void;
|
||||||
onOpenExport: () => void;
|
onOpenExport: () => void;
|
||||||
onOpenHelp: () => void;
|
onOpenHelp: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorShell: React.FC<EditorShellProps> = (props) => {
|
const EditorShell: React.FC<EditorShellProps> = (props) => {
|
||||||
const { project, user, viewMode, currentChapterId, children } = props;
|
const { project, user, viewMode, currentChapterId, children } = props;
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
|
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
|
||||||
|
|
||||||
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
|
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex h-screen overflow-hidden no-print ${user.preferences.theme === 'dark' ? 'bg-slate-900 text-white' : user.preferences.theme === 'sepia' ? 'bg-[#f4ecd8]' : 'bg-[#eef2ff]'}`}>
|
<div className={`flex h-screen overflow-hidden no-print ${user.preferences.theme === 'dark' ? 'bg-slate-900 text-white' : user.preferences.theme === 'sepia' ? 'bg-[#f4ecd8]' : 'bg-[#eef2ff]'}`}>
|
||||||
|
|
||||||
{/* SIDEBAR */}
|
{/* SIDEBAR */}
|
||||||
<aside className={`${isSidebarOpen ? 'w-64' : 'w-0'} bg-slate-900 text-slate-300 flex-shrink-0 transition-all duration-300 overflow-hidden flex flex-col border-r border-slate-800`}>
|
<aside className={`${isSidebarOpen ? 'w-64' : 'w-0'} bg-slate-900 text-slate-300 flex-shrink-0 transition-all duration-300 overflow-hidden flex flex-col border-r border-slate-800`}>
|
||||||
<div className="p-4 border-b border-slate-700">
|
<div className="p-4 border-b border-slate-700">
|
||||||
<h1 className="text-white font-bold flex items-center gap-2 mb-4 cursor-pointer" onClick={() => props.onViewModeChange('dashboard')}>
|
<h1 className="text-white font-bold flex items-center gap-2 mb-4 cursor-pointer" onClick={() => props.onViewModeChange('dashboard')}>
|
||||||
<Book className="text-blue-400" /> PlumeIA
|
<Book className="text-blue-400" /> PlumeIA
|
||||||
</h1>
|
</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
|
||||||
value={project.title}
|
|
||||||
onChange={(e) => props.onUpdateProject({ title: e.target.value })}
|
|
||||||
className="w-full bg-transparent font-serif font-bold text-white text-lg mb-1 focus:outline-none focus:border-b focus:border-blue-500 truncate"
|
|
||||||
placeholder="Titre du livre"
|
|
||||||
/>
|
|
||||||
<button onClick={() => props.onViewModeChange('dashboard')} className="w-full flex items-center gap-2 text-xs hover:bg-slate-800 p-2 rounded transition-colors text-slate-400">
|
|
||||||
<LayoutDashboard size={14} /> Retour au Dashboard
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto py-2">
|
|
||||||
<div className="px-4 py-2 text-xs font-semibold text-slate-500 uppercase flex justify-between items-center">
|
|
||||||
Chapitres <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button>
|
|
||||||
</div>
|
|
||||||
{project.chapters.map((chap, idx) => (
|
|
||||||
<div key={chap.id} className="group relative">
|
|
||||||
<button
|
|
||||||
onClick={() => props.onChapterSelect(chap.id)}
|
|
||||||
className={`w-full text-left px-4 py-2 text-sm truncate transition-colors ${currentChapterId === chap.id && viewMode === 'write' ? 'bg-blue-900 text-white border-r-2 border-blue-400' : 'hover:bg-slate-800'}`}
|
|
||||||
>
|
|
||||||
{idx + 1}. {chap.title}
|
|
||||||
</button>
|
|
||||||
<button onClick={() => props.onDeleteChapter(chap.id)} className="absolute right-2 top-2 text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100"><Trash2 size={14} /></button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">Outils & Bible</div>
|
|
||||||
<button onClick={() => props.onViewModeChange('write')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'write' ? 'bg-blue-900 text-white' : 'hover:bg-slate-800'}`}><FileText size={16} /> Éditeur</button>
|
|
||||||
<button onClick={() => props.onViewModeChange('world_building')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'world_building' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Globe size={16} /> Bible du Monde</button>
|
|
||||||
<button onClick={() => props.onViewModeChange('workflow')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'workflow' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><GitGraph size={16} /> Workflow</button>
|
|
||||||
<button onClick={() => props.onViewModeChange('ideas')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'ideas' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Lightbulb size={16} /> Boîte à Idées</button>
|
|
||||||
<button onClick={() => props.onViewModeChange('settings')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'settings' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Settings size={16} /> Paramètres</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 border-t border-slate-800">
|
|
||||||
<div className="bg-slate-800 rounded-lg p-3 mb-4">
|
|
||||||
<div className="flex justify-between text-[10px] text-slate-400 uppercase font-bold mb-1">
|
|
||||||
<span>Actions IA</span>
|
|
||||||
<span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
|
|
||||||
<div className="h-full bg-blue-500" style={{ width: `${Math.min(100, (user.usage.aiActionsCurrent / user.usage.aiActionsLimit) * 100)}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onClick={() => props.onViewModeChange('profile')} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-slate-400 hover:bg-slate-800 rounded mb-2"><User size={14} /> Mon Compte</button>
|
|
||||||
<button onClick={props.onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-red-400 hover:bg-red-900/20 rounded"><LogOut size={14} /> Déconnexion</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* MAIN CONTENT */}
|
|
||||||
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
|
||||||
<header className="h-14 bg-white border-b border-slate-200 flex items-center justify-between px-4 shadow-sm z-10 text-slate-800">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button onClick={() => setIsSidebarOpen(!isSidebarOpen)} className="text-slate-500 hover:text-slate-800"><Menu size={20} /></button>
|
|
||||||
{viewMode === 'write' ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
value={currentChapter?.title || ""}
|
value={project.title}
|
||||||
onChange={(e) => props.onUpdateProject({ chapters: project.chapters.map(c => c.id === currentChapterId ? {...c, title: e.target.value} : c) })}
|
onChange={(e) => props.onUpdateProject({ title: e.target.value })}
|
||||||
className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none"
|
className="w-full bg-transparent font-serif font-bold text-white text-lg mb-1 focus:outline-none focus:border-b focus:border-blue-500 truncate"
|
||||||
/>
|
placeholder="Titre du livre"
|
||||||
) : (
|
/>
|
||||||
<span className="font-bold uppercase tracking-widest text-xs">{viewMode}</span>
|
<button onClick={() => props.onViewModeChange('dashboard')} className="w-full flex items-center gap-2 text-xs hover:bg-slate-800 p-2 rounded transition-colors text-slate-400">
|
||||||
)}
|
<LayoutDashboard size={14} /> Retour au Dashboard
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center gap-3">
|
</div>
|
||||||
<button onClick={props.onOpenExport} className="bg-blue-600 text-white px-4 py-1.5 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center gap-2"><Share2 size={16} /> Publier</button>
|
|
||||||
<button onClick={props.onOpenHelp} className="p-2 text-slate-400 hover:text-blue-600 rounded-full"><HelpCircle size={20} /></button>
|
|
||||||
<button onClick={() => setIsAiPanelOpen(!isAiPanelOpen)} className={`p-2 rounded-full ${isAiPanelOpen ? 'bg-indigo-100 text-indigo-600' : 'text-slate-500 hover:bg-slate-100'}`}>
|
|
||||||
{isAiPanelOpen ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-y-auto py-2">
|
||||||
{children}
|
<div className="px-4 py-2 text-xs font-semibold text-slate-500 uppercase flex justify-between items-center">
|
||||||
</main>
|
Chapitres <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button>
|
||||||
</div>
|
</div>
|
||||||
|
{project.chapters.map((chap, idx) => (
|
||||||
|
<div key={chap.id} className="group relative">
|
||||||
|
<button
|
||||||
|
onClick={() => props.onChapterSelect(chap.id)}
|
||||||
|
className={`w-full text-left px-4 py-2 text-sm truncate transition-colors ${currentChapterId === chap.id && viewMode === 'write' ? 'bg-blue-900 text-white border-r-2 border-blue-400' : 'hover:bg-slate-800'}`}
|
||||||
|
>
|
||||||
|
{idx + 1}. {chap.title}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.onDeleteChapter(chap.id)} className="absolute right-2 top-2 text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100"><Trash2 size={14} /></button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* AI PANEL */}
|
<div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">Outils & Bible</div>
|
||||||
<div className={`${isAiPanelOpen ? 'w-80 lg:w-96' : 'w-0'} transition-all duration-300 flex-shrink-0 h-full border-l border-slate-200 relative`}>
|
<button onClick={() => props.onViewModeChange('write')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'write' ? 'bg-blue-900 text-white' : 'hover:bg-slate-800'}`}><FileText size={16} /> Retour à la rédaction</button>
|
||||||
{isAiPanelOpen && <AIPanel chatHistory={props.chatHistory} onSendMessage={props.onSendMessage} onInsertText={props.onInsertText} selectedText="" isGenerating={props.isGenerating} usage={user.usage} />}
|
<button onClick={() => props.onViewModeChange('world_building')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'world_building' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Globe size={16} /> Bible du Monde</button>
|
||||||
</div>
|
<button onClick={() => props.onViewModeChange('workflow')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'workflow' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><GitGraph size={16} /> Workflow</button>
|
||||||
</div>
|
<button onClick={() => props.onViewModeChange('ideas')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'ideas' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Lightbulb size={16} /> Boîte à Idées</button>
|
||||||
);
|
<button onClick={() => props.onViewModeChange('settings')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'settings' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Settings size={16} /> Paramètres</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-slate-800">
|
||||||
|
<div className="bg-slate-800 rounded-lg p-3 mb-4">
|
||||||
|
<div className="flex justify-between text-[10px] text-slate-400 uppercase font-bold mb-1">
|
||||||
|
<span>Actions IA</span>
|
||||||
|
<span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-blue-500" style={{ width: `${Math.min(100, (user.usage.aiActionsCurrent / user.usage.aiActionsLimit) * 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => props.onViewModeChange('profile')} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-slate-400 hover:bg-slate-800 rounded mb-2"><User size={14} /> Mon Compte</button>
|
||||||
|
<button onClick={props.onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-red-400 hover:bg-red-900/20 rounded"><LogOut size={14} /> Déconnexion</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* MAIN CONTENT */}
|
||||||
|
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
||||||
|
<header className="h-14 bg-white border-b border-slate-200 flex items-center justify-between px-4 shadow-sm z-10 text-slate-800">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button onClick={() => setIsSidebarOpen(!isSidebarOpen)} className="text-slate-500 hover:text-slate-800"><Menu size={20} /></button>
|
||||||
|
{viewMode === 'write' ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentChapter?.title || ""}
|
||||||
|
onChange={(e) => props.onUpdateProject({ chapters: project.chapters.map(c => c.id === currentChapterId ? { ...c, title: e.target.value } : c) })}
|
||||||
|
className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="font-bold uppercase tracking-widest text-xs">{viewMode}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button onClick={props.onOpenExport} className="bg-blue-600 text-white px-4 py-1.5 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center gap-2"><Share2 size={16} /> Publier</button>
|
||||||
|
<button onClick={props.onOpenHelp} className="p-2 text-slate-400 hover:text-blue-600 rounded-full"><HelpCircle size={20} /></button>
|
||||||
|
<button onClick={() => setIsAiPanelOpen(!isAiPanelOpen)} className={`p-2 rounded-full ${isAiPanelOpen ? 'bg-indigo-100 text-indigo-600' : 'text-slate-500 hover:bg-slate-100'}`}>
|
||||||
|
{isAiPanelOpen ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-hidden relative">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI PANEL */}
|
||||||
|
<div className={`${isAiPanelOpen ? 'w-80 lg:w-96' : 'w-0'} transition-all duration-300 flex-shrink-0 h-full border-l border-slate-200 relative`}>
|
||||||
|
{isAiPanelOpen && <AIPanel chatHistory={props.chatHistory} onSendMessage={props.onSendMessage} onInsertText={props.onInsertText} selectedText="" isGenerating={props.isGenerating} usage={user.usage} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditorShell;
|
export default EditorShell;
|
||||||
|
|||||||
34
hooks.ts
34
hooks.ts
@@ -291,18 +291,32 @@ export function useProjects(user: UserProfile | null) {
|
|||||||
|
|
||||||
|
|
||||||
const addChapter = async () => {
|
const addChapter = async () => {
|
||||||
if (!currentProjectId) return null;
|
console.log("[Hooks] addChapter called. Current Project ID:", currentProjectId);
|
||||||
|
if (!currentProjectId) {
|
||||||
|
console.error("[Hooks] addChapter failed: No currentProjectId");
|
||||||
|
alert("Erreur: Impossible d'ajouter un chapitre car aucun projet n'est actif.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const projectId = parseInt(currentProjectId);
|
const projectId = parseInt(currentProjectId);
|
||||||
const result = await dataService.createItem('chapters', {
|
console.log("[Hooks] Creating chapter for project:", projectId);
|
||||||
project_id: projectId,
|
|
||||||
title: "Nouveau Chapitre",
|
|
||||||
content: "<p>Contenu...</p>",
|
|
||||||
summary: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status === 'success') {
|
try {
|
||||||
await fetchProjectDetails(currentProjectId);
|
const result = await dataService.createItem('chapters', {
|
||||||
return result.id.toString();
|
project_id: projectId,
|
||||||
|
title: "Nouveau Chapitre",
|
||||||
|
content: "<p>Contenu...</p>",
|
||||||
|
summary: ""
|
||||||
|
});
|
||||||
|
console.log("[Hooks] createItem result:", result);
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
await fetchProjectDetails(currentProjectId);
|
||||||
|
return result.id.toString();
|
||||||
|
} else {
|
||||||
|
console.error("[Hooks] createItem failed status:", result);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Hooks] createItem exception:", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
222
services/api.ts
222
services/api.ts
@@ -2,27 +2,48 @@
|
|||||||
import { BookProject, Chapter, Entity, Idea, WorkflowData, UserProfile } from '../types';
|
import { BookProject, Chapter, Entity, Idea, WorkflowData, UserProfile } from '../types';
|
||||||
|
|
||||||
// --- CONFIGURATION ---
|
// --- CONFIGURATION ---
|
||||||
const AUTH_API_ROOT = 'https://app.nocodebackend.com/api/user-auth';
|
const AUTH_API_ROOT = '/api/user-auth';
|
||||||
const DATA_API_ROOT = 'https://app.nocodebackend.com/api/data';
|
const DATA_API_ROOT = '/api/data';
|
||||||
const INSTANCE_ID = '54770_plumeia_db';
|
const INSTANCE_ID = '54770_plumeia_db';
|
||||||
|
|
||||||
// --- HELPERS ---
|
// --- HELPERS ---
|
||||||
|
|
||||||
const getHeaders = () => {
|
// --- HELPERS ---
|
||||||
const token = localStorage.getItem('ncb_session_token');
|
|
||||||
|
|
||||||
|
const getHeaders = () => {
|
||||||
|
/*const token = localStorage.getItem('ncb_session_token');
|
||||||
|
const sessionData = localStorage.getItem('ncb_session_data');
|
||||||
|
console.log("[API] Token:", token);
|
||||||
|
console.log("[API] Session Data:", sessionData);*/
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Database-Instance': INSTANCE_ID
|
'X-Database-Instance': INSTANCE_ID,
|
||||||
|
// 'credentials': 'include'
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
|
if (token) {
|
||||||
|
// Fallback standard auth
|
||||||
|
//headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
|
||||||
if (token) {
|
// User-requested specific Cookie format
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
// Note: Browsers typically block manual "Cookie" header setting in fetch.
|
||||||
}
|
// This is implemented per user request, but relies on the environment allowing it
|
||||||
|
// or using credentials: 'include' for actual cookies.
|
||||||
|
let cookieString = `better-auth.session_token=${token}`;
|
||||||
|
if (sessionData) {
|
||||||
|
cookieString += `; better-auth.session_data=${sessionData}`;
|
||||||
|
}
|
||||||
|
headers['Cookie'] = cookieString;
|
||||||
|
|
||||||
|
console.log("[API] Cookie:", cookieString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug headers
|
||||||
|
console.log("[API] Generated Headers:", headers);
|
||||||
|
*/
|
||||||
return headers;
|
return headers;
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
const handleAuthResponse = async (res: Response) => {
|
const handleAuthResponse = async (res: Response) => {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log("[API] Auth Response:", data);
|
console.log("[API] Auth Response:", data);
|
||||||
@@ -31,7 +52,7 @@ const handleAuthResponse = async (res: Response) => {
|
|||||||
throw new Error(data.message || 'Authentication failed');
|
throw new Error(data.message || 'Authentication failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token extraction strategy based on NCB patterns
|
// Token extraction strategy
|
||||||
const token =
|
const token =
|
||||||
data.user?.token ||
|
data.user?.token ||
|
||||||
data.token ||
|
data.token ||
|
||||||
@@ -40,30 +61,74 @@ const handleAuthResponse = async (res: Response) => {
|
|||||||
data.accessToken ||
|
data.accessToken ||
|
||||||
data._token;
|
data._token;
|
||||||
|
|
||||||
|
// Extract session data if available (often needed for Better Auth)
|
||||||
|
const sessionData = data.session_data || data.session?.data || data.user?.session_data;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log("[API] Token extracted and saved:", token);
|
console.log("[API] Token extracted and saved:", token);
|
||||||
localStorage.setItem('ncb_session_token', token);
|
localStorage.setItem('ncb_session_token', token);
|
||||||
|
|
||||||
|
if (sessionData) {
|
||||||
|
console.log("[API] Session Data extracted and saved");
|
||||||
|
localStorage.setItem('ncb_session_data', sessionData);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("[API] No token found in successful auth response!");
|
console.warn("[API] No token found in successful auth response!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};*/
|
||||||
|
|
||||||
|
const handleAuthResponse = async (res: Response) => {
|
||||||
|
const data = await res.json();
|
||||||
|
console.log("[API] Raw Response Data:", data);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.message || 'Authentication failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LOGIQUE DES SETTEURS ---
|
||||||
|
|
||||||
|
// 1. Extraction du Token (selon la structure Better-Auth)
|
||||||
|
const token = data.session?.token || data.token;
|
||||||
|
|
||||||
|
// 2. Extraction du Session Data
|
||||||
|
// On prend l'objet session complet et on le stringifie pour le stockage
|
||||||
|
const sessionData = data.session ? JSON.stringify(data.session) : null;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('ncb_session_token', token);
|
||||||
|
console.log("[Auth] Token saved to LocalStorage");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionData) {
|
||||||
|
localStorage.setItem('ncb_session_data', sessionData);
|
||||||
|
console.log("[Auth] Session Data saved to LocalStorage");
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- AUTH SERVICE ---
|
// --- AUTH SERVICE ---
|
||||||
|
|
||||||
export const authService = {
|
export const authService = {
|
||||||
async getSession() {
|
/*async getSession() {
|
||||||
const token = localStorage.getItem('ncb_session_token');
|
const token = localStorage.getItem('ncb_session_token');
|
||||||
if (!token) return null;
|
// Note: Even if we use cookies, we keep token logic as fallback or for UI state
|
||||||
|
// if (!token) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${AUTH_API_ROOT}/get-session?Instance=${INSTANCE_ID}`, {
|
const url = `${AUTH_API_ROOT}/get-session?Instance=${INSTANCE_ID}`;
|
||||||
|
console.log(`[API] GET session ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getHeaders()
|
//headers: getHeaders(),
|
||||||
|
credentials: 'include' // IMPORTANT: Send cookies
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
console.log(`[API] getSession failed with status: ${res.status}`);
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
localStorage.removeItem('ncb_session_token');
|
localStorage.removeItem('ncb_session_token');
|
||||||
}
|
}
|
||||||
@@ -71,26 +136,77 @@ export const authService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
console.log("[API] getSession success:", data);
|
||||||
return data.user || data;
|
return data.user || data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Auth] getSession error:", err);
|
console.error("[Auth] getSession error:", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
},*/
|
||||||
|
|
||||||
|
async getSession() {
|
||||||
|
console.log("[getSession] démarrage");
|
||||||
|
const token = localStorage.getItem('ncb_session_token');
|
||||||
|
try {
|
||||||
|
const url = `${AUTH_API_ROOT}/get-session?Instance=${INSTANCE_ID}`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Database-Instance': INSTANCE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si on a un token mais pas encore la session complète, on l'envoie
|
||||||
|
if (token) {
|
||||||
|
headers['Cookie'] = `better-auth.session_token=${token}`;
|
||||||
|
// On peut aussi essayer le header Authorization au cas où
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: headers,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
console.log("[getSession] getSession success:", data);
|
||||||
|
// --- LES SETTEURS ICI ---
|
||||||
|
if (data.session) {
|
||||||
|
localStorage.setItem('ncb_session_token', data.session.token);
|
||||||
|
localStorage.setItem('ncb_session_data', JSON.stringify(data.session));
|
||||||
|
console.log("[getSession] Données récupérées depuis getSession");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.user || data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Auth] getSession error:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async signIn(email: string, password: string) {
|
async signIn(email: string, password: string) {
|
||||||
try {
|
try {
|
||||||
// Force X-Database-Instance header as URL param is insufficient for some NCB versions
|
// Force X-Database-Instance header as URL param is insufficient for some NCB versions
|
||||||
const res = await fetch(`${AUTH_API_ROOT}/sign-in/email?Instance=${INSTANCE_ID}`, {
|
const url = `${AUTH_API_ROOT}/sign-in/email?Instance=${INSTANCE_ID}`;
|
||||||
|
console.log(`[API] POST ${url}`, { email, password: '***' });
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Database-Instance': INSTANCE_ID
|
'X-Database-Instance': INSTANCE_ID
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, password })
|
body: JSON.stringify({ email, password }),
|
||||||
|
credentials: 'include' // IMPORTANT: Receive & Save cookies
|
||||||
});
|
});
|
||||||
|
const authData = await handleAuthResponse(res);
|
||||||
|
await this.getSession();
|
||||||
|
console.log("sign in", res);
|
||||||
|
|
||||||
return await handleAuthResponse(res);
|
return await authData;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("[Auth] signIn error:", err);
|
console.error("[Auth] signIn error:", err);
|
||||||
return { error: err.message || 'Connection failed' };
|
return { error: err.message || 'Connection failed' };
|
||||||
@@ -99,26 +215,35 @@ export const authService = {
|
|||||||
|
|
||||||
async signUp(email: string, password: string, name: string) {
|
async signUp(email: string, password: string, name: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${AUTH_API_ROOT}/sign-up/email?Instance=${INSTANCE_ID}`, {
|
const url = `${AUTH_API_ROOT}/sign-up/email?Instance=${INSTANCE_ID}`;
|
||||||
|
console.log(`[API] POST ${url}`, { email, name });
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Database-Instance': INSTANCE_ID
|
'X-Database-Instance': INSTANCE_ID
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, password, name })
|
body: JSON.stringify({ email, password, name }),
|
||||||
|
credentials: 'include' // IMPORTANT: Receive & Save cookies
|
||||||
});
|
});
|
||||||
|
|
||||||
return await handleAuthResponse(res);
|
return await handleAuthResponse(res);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error("[Auth] signUp error:", err);
|
||||||
return { error: err.message || 'Registration failed' };
|
return { error: err.message || 'Registration failed' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
try {
|
try {
|
||||||
await fetch(`${AUTH_API_ROOT}/sign-out?Instance=${INSTANCE_ID}`, {
|
const url = `${AUTH_API_ROOT}/sign-out?Instance=${INSTANCE_ID}`;
|
||||||
|
console.log(`[API] POST ${url}`);
|
||||||
|
|
||||||
|
await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include' // IMPORTANT: Clear cookies
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Auth] signOut error:", err);
|
console.error("[Auth] signOut error:", err);
|
||||||
@@ -137,8 +262,14 @@ export const dataService = {
|
|||||||
try {
|
try {
|
||||||
const encodedId = encodeURIComponent(userId);
|
const encodedId = encodeURIComponent(userId);
|
||||||
const url = `${DATA_API_ROOT}/read/profiles?Instance=${INSTANCE_ID}&user_id=${encodedId}`;
|
const url = `${DATA_API_ROOT}/read/profiles?Instance=${INSTANCE_ID}&user_id=${encodedId}`;
|
||||||
const res = await fetch(url, { headers: getHeaders() });
|
console.log(`[API] GET ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
console.log("[Data] getProfile result:", json);
|
||||||
return json.data?.[0] || null;
|
return json.data?.[0] || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Data] getProfile error:", err);
|
console.error("[Data] getProfile error:", err);
|
||||||
@@ -155,8 +286,14 @@ export const dataService = {
|
|||||||
try {
|
try {
|
||||||
const encodedId = encodeURIComponent(userId);
|
const encodedId = encodeURIComponent(userId);
|
||||||
const url = `${DATA_API_ROOT}/read/projects?Instance=${INSTANCE_ID}&user_id=${encodedId}`;
|
const url = `${DATA_API_ROOT}/read/projects?Instance=${INSTANCE_ID}&user_id=${encodedId}`;
|
||||||
const res = await fetch(url, { headers: getHeaders() });
|
console.log(`[API] GET ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
console.log(`[Data] getProjects found ${json.data?.length || 0} items`);
|
||||||
return json.data || [];
|
return json.data || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Data] getProjects error:", err);
|
console.error("[Data] getProjects error:", err);
|
||||||
@@ -175,8 +312,14 @@ export const dataService = {
|
|||||||
async getRelatedData(table: string, projectId: number) {
|
async getRelatedData(table: string, projectId: number) {
|
||||||
try {
|
try {
|
||||||
const url = `${DATA_API_ROOT}/read/${table}?Instance=${INSTANCE_ID}&project_id=${projectId}`;
|
const url = `${DATA_API_ROOT}/read/${table}?Instance=${INSTANCE_ID}&project_id=${projectId}`;
|
||||||
const res = await fetch(url, { headers: getHeaders() });
|
console.log(`[API] GET ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
console.log(`[Data] getRelatedData ${table} found ${json.data?.length || 0} items`);
|
||||||
return json.data || [];
|
return json.data || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Data] getRelatedData ${table} error:`, err);
|
console.error(`[Data] getRelatedData ${table} error:`, err);
|
||||||
@@ -188,6 +331,7 @@ export const dataService = {
|
|||||||
try {
|
try {
|
||||||
console.log(`[Data] Creating item in ${table}...`, data);
|
console.log(`[Data] Creating item in ${table}...`, data);
|
||||||
const url = `${DATA_API_ROOT}/create/${table}?Instance=${INSTANCE_ID}`;
|
const url = `${DATA_API_ROOT}/create/${table}?Instance=${INSTANCE_ID}`;
|
||||||
|
console.log(`[API] POST ${url}`, data);
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -197,6 +341,8 @@ export const dataService = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
console.log(`[Data] Create ${table} response:`, result);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error(`[Data] Create ${table} failed:`, result);
|
console.error(`[Data] Create ${table} failed:`, result);
|
||||||
return { status: 'error', message: result.message || 'Creation failed' };
|
return { status: 'error', message: result.message || 'Creation failed' };
|
||||||
@@ -211,11 +357,21 @@ export const dataService = {
|
|||||||
async updateItem(table: string, id: number, data: any) {
|
async updateItem(table: string, id: number, data: any) {
|
||||||
try {
|
try {
|
||||||
const url = `${DATA_API_ROOT}/update/${table}/${id}?Instance=${INSTANCE_ID}`;
|
const url = `${DATA_API_ROOT}/update/${table}/${id}?Instance=${INSTANCE_ID}`;
|
||||||
await fetch(url, {
|
console.log(`[API] PUT ${url}`, data);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: getHeaders(),
|
headers: getHeaders(),
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
console.error(`[Data] Update ${table} failed:`, err);
|
||||||
|
} else {
|
||||||
|
console.log(`[Data] Update ${table} success`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Data] Update ${table} error:`, err);
|
console.error(`[Data] Update ${table} error:`, err);
|
||||||
}
|
}
|
||||||
@@ -224,10 +380,20 @@ export const dataService = {
|
|||||||
async deleteItem(table: string, id: number) {
|
async deleteItem(table: string, id: number) {
|
||||||
try {
|
try {
|
||||||
const url = `${DATA_API_ROOT}/delete/${table}/${id}?Instance=${INSTANCE_ID}`;
|
const url = `${DATA_API_ROOT}/delete/${table}/${id}?Instance=${INSTANCE_ID}`;
|
||||||
await fetch(url, {
|
console.log(`[API] DELETE ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getHeaders()
|
headers: getHeaders(),
|
||||||
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
console.error(`[Data] Delete ${table} failed:`, err);
|
||||||
|
} else {
|
||||||
|
console.log(`[Data] Delete ${table} success`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Data] Delete ${table} error:`, err);
|
console.error(`[Data] Delete ${table} error:`, err);
|
||||||
}
|
}
|
||||||
|
|||||||
4
types.ts
4
types.ts
@@ -107,6 +107,10 @@ export interface BookProject {
|
|||||||
author: string;
|
author: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
settings?: BookSettings;
|
settings?: BookSettings;
|
||||||
|
// Direct fields sometimes used in creation/updates before settings normalization
|
||||||
|
genre?: string;
|
||||||
|
pov?: string;
|
||||||
|
tense?: string;
|
||||||
chapters: Chapter[];
|
chapters: Chapter[];
|
||||||
entities: Entity[];
|
entities: Entity[];
|
||||||
workflow?: WorkflowData;
|
workflow?: WorkflowData;
|
||||||
|
|||||||
@@ -2,6 +2,45 @@ import path from 'path';
|
|||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, '.', '');
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
// --- AJOUT DU PROXY ICI ---
|
||||||
|
proxy: {
|
||||||
|
'/api/user-auth': {
|
||||||
|
target: 'https://app.nocodebackend.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
cookieDomainRewrite: "localhost",
|
||||||
|
},
|
||||||
|
'/api/data': {
|
||||||
|
target: 'https://app.nocodebackend.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
cookieDomainRewrite: "localhost",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/*mport path from 'path';
|
||||||
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, '.', '');
|
const env = loadEnv(mode, '.', '');
|
||||||
return {
|
return {
|
||||||
@@ -21,3 +60,4 @@ export default defineConfig(({ mode }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user