From be5bd2b2bf83435df1e3eb3e608ded3e14737a1d Mon Sep 17 00:00:00 2001 From: streaper2 Date: Sun, 8 Feb 2026 16:12:25 +0100 Subject: [PATCH] authentification nocodebackend ok --- .gitignore | 24 + App.tsx | 130 ++ AuthContext.tsx | 14 + README.md | 20 + components/AIPanel.tsx | 111 ++ components/AppRouter.tsx | 162 ++ components/AuthPage.tsx | 202 ++ components/BookSettings.tsx | 214 +++ components/Checkout.tsx | 63 + components/Dashboard.tsx | 154 ++ components/ExportModal.tsx | 246 +++ components/FeaturesPage.tsx | 42 + components/HelpModal.tsx | 283 +++ components/IdeaBoard.tsx | 371 ++++ components/LandingPage.tsx | 93 + components/LoginPage.tsx | 105 ++ components/Pricing.tsx | 59 + components/RichTextEditor.tsx | 572 ++++++ components/StoryWorkflow.tsx | 680 +++++++ components/UserProfileSettings.tsx | 203 ++ components/WorldBuilder.tsx | 722 +++++++ components/layout/EditorShell.tsx | 132 ++ constants.ts | 67 + hooks.bak copy.ts | 385 ++++ hooks.ts | 416 ++++ index.html | 109 ++ index.tsx | 15 + metadata.json | 5 + package-lock.json | 2826 ++++++++++++++++++++++++++++ package.json | 25 + services/api.backup.ts | 264 +++ services/api.bak2.ts | 233 +++ services/api.ts | 235 +++ services/geminiService.ts | 185 ++ tsconfig.json | 29 + types.ts | 166 ++ vite.config.ts | 23 + 37 files changed, 9585 insertions(+) create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 AuthContext.tsx create mode 100644 README.md create mode 100644 components/AIPanel.tsx create mode 100644 components/AppRouter.tsx create mode 100644 components/AuthPage.tsx create mode 100644 components/BookSettings.tsx create mode 100644 components/Checkout.tsx create mode 100644 components/Dashboard.tsx create mode 100644 components/ExportModal.tsx create mode 100644 components/FeaturesPage.tsx create mode 100644 components/HelpModal.tsx create mode 100644 components/IdeaBoard.tsx create mode 100644 components/LandingPage.tsx create mode 100644 components/LoginPage.tsx create mode 100644 components/Pricing.tsx create mode 100644 components/RichTextEditor.tsx create mode 100644 components/StoryWorkflow.tsx create mode 100644 components/UserProfileSettings.tsx create mode 100644 components/WorldBuilder.tsx create mode 100644 components/layout/EditorShell.tsx create mode 100644 constants.ts create mode 100644 hooks.bak copy.ts create mode 100644 hooks.ts create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 services/api.backup.ts create mode 100644 services/api.bak2.ts create mode 100644 services/api.ts create mode 100644 services/geminiService.ts create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..2dae900 --- /dev/null +++ b/App.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { ViewMode } from './types'; +import { useProjects, useChat } from './hooks'; +import { AuthProvider, useAuthContext } from './AuthContext'; +import AppRouter from './components/AppRouter'; +import { Loader2, BookOpen } from 'lucide-react'; + +/** + * MainContent : Ce composant est "sous" le AuthProvider. + * Il peut donc utiliser useAuthContext() sans recevoir de 'null'. + */ +const MainContent: React.FC = () => { + const [viewMode, setViewMode] = useState('landing'); + + // --- 1. DÉCLARATION DE TOUS LES HOOKS (IMPÉRATIF : TOUJOURS EN HAUT) --- + // On récupère l'état global partagé via le Contexte + const { + user, + login, + signup, + logout, + incrementUsage, + loading: authLoading + } = useAuthContext(); + + // On initialise les projets. Si user est null, useProjects retournera une liste vide. + const { + projects, currentProjectId, setCurrentProjectId, + createProject, updateProject, updateChapter, addChapter, + createEntity, updateEntity, deleteEntity + } = useProjects(user); + + // On initialise le chat + const { chatHistory, isGenerating, sendMessage } = useChat(); + + // --- 2. LOGIQUE DE CALCUL --- + const currentProject = projects.find(p => p.id === currentProjectId); + + const handleSendMessage = (msg: string) => { + if (currentProject && user) { + sendMessage(currentProject, 'global', msg, user, incrementUsage); + } + }; + + // --- 3. RENDU CONDITIONNEL (Seulement APRES les hooks) --- + // On attend que l'Auth + Profil soient chargés (tes 2 secondes d'attente) + if (authLoading) { + return ( +
+
+ +
+
+
+ + PlumeIA +
+

+ Synchronisation de votre espace créatif... +

+
+ ); + } + + // --- 4. RENDU FINAL : AppRouter --- + return ( + { + logout(); + setViewMode('landing'); + }} + + // Gestion des Projets + projects={projects} + currentProjectId={currentProjectId} + onSelectProject={(id) => { + setCurrentProjectId(id); + setViewMode('write'); + }} + onCreateProject={async () => { + const id = await createProject(); + if (id) { + setCurrentProjectId(id); + setViewMode('write'); + } + }} + onUpdateProject={updateProject} + + // Gestion du contenu (Chapitres & Entités) + onUpdateChapter={updateChapter} + onAddChapter={addChapter} + onCreateEntity={createEntity} + onUpdateEntity={updateEntity} + onDeleteEntity={deleteEntity} + + // Chat & IA + chatHistory={chatHistory} + isGenerating={isGenerating} + onSendMessage={handleSendMessage} + onIncrementUsage={incrementUsage} + + // Callbacks divers + onUpdateProfile={() => console.log("Profil géré via useAuth")} + onUpgradePlan={() => window.open('/billing', '_blank')} + /> + ); +}; + +/** + * Composant App : Point d'entrée de l'application. + * Il installe la "bulle" AuthProvider pour que tout le monde y ait accès. + */ +const App: React.FC = () => { + return ( + + + + ); +}; + +// EXPORT PAR DÉFAUT (Essentiel pour index.tsx) +export default App; \ No newline at end of file diff --git a/AuthContext.tsx b/AuthContext.tsx new file mode 100644 index 0000000..e0d1ba3 --- /dev/null +++ b/AuthContext.tsx @@ -0,0 +1,14 @@ +import React, { createContext, useContext } from 'react'; +import { useAuth as useAuthHook } from './hooks'; + +const AuthContext = createContext(null); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const auth = useAuthHook(); + return {children}; +}; + +// Ce hook permettra d'accéder à l'auth n'importe où +export function useAuthContext() { + return useContext(AuthContext); +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4dd664e --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1F6iXw8LXUE67VTaem9IfxgM5UMR_dvMV + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/AIPanel.tsx b/components/AIPanel.tsx new file mode 100644 index 0000000..4df935f --- /dev/null +++ b/components/AIPanel.tsx @@ -0,0 +1,111 @@ + +import React, { useState, useEffect, useRef } from 'react'; +import { Sparkles, Send, RefreshCw, BookOpen, Bot, ArrowLeft, BrainCircuit, Zap } from 'lucide-react'; +import { ChatMessage, UserUsage } from '../types'; + +interface AIPanelProps { + chatHistory: ChatMessage[]; + onSendMessage: (msg: string) => void; + onInsertText: (text: string) => void; + selectedText: string; + isGenerating: boolean; + usage?: UserUsage; +} + +const AIPanel: React.FC = ({ chatHistory, onSendMessage, onInsertText, selectedText, isGenerating, usage }) => { + const [input, setInput] = useState(""); + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [chatHistory, isGenerating]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || isGenerating) return; + onSendMessage(input); + setInput(""); + }; + + const isLimitReached = usage ? usage.aiActionsCurrent >= usage.aiActionsLimit : false; + + return ( +
+ {/* Header with Usage Counter */} +
+
+ +

Assistant IA

+
+ {usage && ( +
+ {usage.aiActionsCurrent} / {usage.aiActionsLimit === 999999 ? '∞' : usage.aiActionsLimit} +
+ )} +
+ + {selectedText && ( +
+
Contexte :
+
"{selectedText.substring(0, 60)}..."
+
+ )} + +
+ {chatHistory.length === 0 && ( +
+ +

Bonjour ! Comment puis-je vous aider aujourd'hui ?

+ {isLimitReached && ( +
+ Limite atteinte ! Améliorez votre plan. +
+ )} +
+ )} + + {chatHistory.map((msg) => ( +
+
+ {msg.role === 'model' && msg.responseType === 'reflection' && ( +
Réflexion
+ )} +
{msg.text}
+
+
+ ))} + + {isGenerating && ( +
+
+ L'IA travaille... +
+
+ )} +
+
+ +
+
+ setInput(e.target.value)} + placeholder={isLimitReached ? "Limite atteinte..." : "Votre message..."} + className="w-full pl-4 pr-12 py-3 bg-slate-100 rounded-2xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-shadow disabled:opacity-50" + disabled={isGenerating || isLimitReached} + /> + +
+
+
+ ); +}; + +export default AIPanel; diff --git a/components/AppRouter.tsx b/components/AppRouter.tsx new file mode 100644 index 0000000..285d53f --- /dev/null +++ b/components/AppRouter.tsx @@ -0,0 +1,162 @@ + +import React, { useState, useRef } from 'react'; +import { ViewMode, BookProject, UserProfile, Chapter, Entity } from '../types'; +import LandingPage from './LandingPage'; +import FeaturesPage from './FeaturesPage'; +import Pricing from './Pricing'; +import Checkout from './Checkout'; +import AuthPage from './AuthPage'; +import LoginPage from './LoginPage'; +import Dashboard from './Dashboard'; +import UserProfileSettings from './UserProfileSettings'; +import ExportModal from './ExportModal'; +import HelpModal from './HelpModal'; +import EditorShell from './layout/EditorShell'; +import RichTextEditor, { RichTextEditorHandle } from './RichTextEditor'; +import WorldBuilder from './WorldBuilder'; +import StoryWorkflow from './StoryWorkflow'; +import IdeaBoard from './IdeaBoard'; +import BookSettingsComponent from './BookSettings'; +import { transformText } from '../services/geminiService'; + +interface AppRouterProps { + user: UserProfile | null; + projects: BookProject[]; + currentProjectId: string | null; + viewMode: ViewMode; + chatHistory: any[]; + isGenerating: boolean; + onLogin: (data: any) => Promise; + onSignup: (data: any) => Promise; + onLogout: () => void; + onViewModeChange: (mode: ViewMode) => void; + onSelectProject: (id: string) => void; + onCreateProject: () => void; + onUpdateProject: (updates: Partial) => void; + onUpdateChapter: (chapterId: string, updates: Partial) => void; + onAddChapter: () => Promise; + onCreateEntity: (entity: Omit) => Promise; + onUpdateEntity: (entityId: string, updates: Partial) => void; + onDeleteEntity: (entityId: string) => void; + onUpdateProfile: (updates: Partial) => void; + onUpgradePlan: (plan: any) => void; + onSendMessage: (msg: string) => void; + onIncrementUsage: () => void; +} + +const AppRouter: React.FC = (props) => { + const [currentChapterId, setCurrentChapterId] = useState(''); + const [isExportModalOpen, setIsExportModalOpen] = useState(false); + const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); + const [targetEntityId, setTargetEntityId] = useState(null); + const editorRef = useRef(null); + + const { user, viewMode, currentProjectId, projects } = props; + const project = projects.find(p => p.id === currentProjectId); + console.log('props', props); + // DEBUG: Check all props + console.log("[AppRouter DEBUG] PROPS RECEIVED:", { + user, + userId: user?.id, + viewMode, + currentProjectId, + projectsCount: projects?.length, + fullProps: props + }); + React.useEffect(() => { + if (project && (!currentChapterId || !project.chapters.some(c => c.id === currentChapterId))) { + setCurrentChapterId(project.chapters[0]?.id || ''); + } + }, [currentProjectId, project]); + + if (viewMode === 'landing') return props.onViewModeChange('auth')} onFeatures={() => props.onViewModeChange('features')} onPricing={() => props.onViewModeChange('pricing')} />; + + // Use new LoginPage for 'auth' view (Login) + if (viewMode === 'auth') return props.onViewModeChange('dashboard')} onRegister={() => props.onViewModeChange('signup')} />; + + // Use existing AuthPage for 'signup' view (Register) - defaulted to signup mode inside AuthPage if possible, + // but AuthPage manages its own state. We can pass a prop if AuthPage supports it, or just let user toggle. + // Since AuthPage has internal state for mode, we might just render it. + // Ideally AuthPage should accept an initialMode prop. Let's check AuthPage again or just render it. + if (viewMode === 'signup') return props.onViewModeChange('landing')} onSuccess={() => props.onViewModeChange('dashboard')} initialMode='signup' />; + if (viewMode === 'features') return props.onViewModeChange(user ? 'dashboard' : 'landing')} />; + if (viewMode === 'pricing') return props.onViewModeChange(user ? 'dashboard' : 'landing')} onSelectPlan={() => user ? props.onViewModeChange('checkout') : props.onViewModeChange('auth')} />; + if (viewMode === 'checkout') return props.onUpgradePlan('pro')} onCancel={() => props.onViewModeChange('pricing')} />; + if (viewMode === 'dashboard' && user) return props.onViewModeChange('pricing')} onProfile={() => props.onViewModeChange('profile')} />; + if (viewMode === 'profile' && user) return props.onViewModeChange('dashboard')} />; + + console.log("[AppRouter] Render State:", { viewMode, hasUser: !!user, hasProject: !!project, currentProjectId, currentChapterId }); + + if (!project || !user) { + // If we are here, we are in a protected route ('write', 'world_building', etc.) + // BUT we don't have a project or user. This is an invalid state. + console.warn("[AppRouter] Fallthrough to NULL - displaying fallback"); + return ( +
+

État Indéfini

+

Mode: {viewMode}

+

Utilisateur: {user ? 'Connecté' : 'Non connecté'}

+

Projet sélectionné: {currentProjectId || 'Aucun'}

+ +
+ ); + } + + const currentChapter = project.chapters.find(c => c.id === currentChapterId); + + return ( + { setCurrentChapterId(id); props.onViewModeChange('write'); }} + onUpdateProject={props.onUpdateProject} + onAddChapter={async () => { + const id = await props.onAddChapter(); + if (id) setCurrentChapterId(id); + }} + onDeleteChapter={(id) => { + if (project.chapters.length > 1) { + const newChapters = project.chapters.filter(c => c.id !== id); + props.onUpdateProject({ chapters: newChapters }); + if (currentChapterId === id) setCurrentChapterId(newChapters[0].id); + } + }} + onLogout={props.onLogout} + onSendMessage={props.onSendMessage} + onInsertText={(text) => editorRef.current?.insertHtml(text)} + onOpenExport={() => setIsExportModalOpen(true)} + onOpenHelp={() => setIsHelpModalOpen(true)} + > + setIsExportModalOpen(false)} project={project} onPrint={() => { }} /> + setIsHelpModalOpen(false)} viewMode={viewMode} /> + + {viewMode === 'write' && props.onUpdateChapter(currentChapterId, { content: html })} + onAiTransform={(text, mode) => transformText(text, mode, currentChapter?.content || "", user, props.onIncrementUsage)} + />} + {viewMode === 'world_building' && props.onUpdateProject({ templates: t })} + initialSelectedId={targetEntityId} + />} + {viewMode === 'ideas' && props.onUpdateProject({ ideas: i })} />} + {viewMode === 'workflow' && props.onUpdateProject({ workflow: w })} entities={project.entities} onNavigateToEntity={(id) => { setTargetEntityId(id); props.onViewModeChange('world_building'); }} />} + {viewMode === 'settings' && } + + ); +}; + +export default AppRouter; diff --git a/components/AuthPage.tsx b/components/AuthPage.tsx new file mode 100644 index 0000000..6f84cbe --- /dev/null +++ b/components/AuthPage.tsx @@ -0,0 +1,202 @@ +import React, { useState, useEffect } from 'react'; +import { Mail, Lock, User, ArrowRight, Loader2, BookOpen, ShieldCheck } from 'lucide-react'; +import { useAuth } from '../hooks'; + +interface AuthPageProps { + onBack: () => void; + onSuccess: () => void; + initialMode?: 'signin' | 'signup' | 'forgot'; +} + +const AuthPage: React.FC = ({ onBack, onSuccess, initialMode = 'signin' }) => { + const [mode, setMode] = useState<'signin' | 'signup' | 'forgot'>(initialMode); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ name: '', email: '', password: '' }); + const [error, setError] = useState(''); + + // On récupère les fonctions de connexion directement du hook + const { user, login, signup } = useAuth(); + + // Redirection automatique dès que l'utilisateur est détecté dans l'état global + useEffect(() => { + if (user) { + onSuccess(); + } + }, [user, onSuccess]); + + const handleAdminLogin = async () => { + const adminData = { email: 'streaper2@gmail.com', password: 'Kency1313' }; + setFormData({ name: 'Admin Plume', ...adminData }); + setLoading(true); + setError(''); + + try { + const result = await login(adminData); + if (result?.error) setError(result.error); + } catch (e) { + setError('Erreur de connexion au service.'); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + let result; + if (mode === 'signup') { + result = await signup(formData); + } else { + result = await login({ email: formData.email, password: formData.password }); + } + + if (result?.error) { + setError(result.error); + } + } catch (e) { + setError('Une erreur technique est survenue.'); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Panneau Latéral (Visible sur Desktop) */} +
+
+
+
+
+ +
+ PlumeIA +
+ +
+

+ L'endroit où vos histoires prennent vie. +

+

+ Rejoignez une communauté d'auteurs qui utilisent l'IA pour briser la page blanche. +

+
+ +
+ © 2024 PlumeIA Ecosystem. +
+
+ + {/* Formulaire */} +
+
+
+

+ {mode === 'signin' ? 'Content de vous revoir' : mode === 'signup' ? "Commencer l'aventure" : 'Récupération'} +

+

+ {mode === 'signin' ? 'Entrez vos identifiants pour continuer.' : 'Créez votre compte gratuit en quelques secondes.'} +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {mode === 'signup' && ( +
+ +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Marc Dupré" + className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" + /> +
+
+ )} + +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="votre@email.com" + className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" + /> +
+
+ + {mode !== 'forgot' && ( +
+ +
+ + setFormData({ ...formData, password: e.target.value })} + placeholder="••••••••" + className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" + /> +
+
+ )} + + +
+ + {mode === 'signin' && ( + + )} + +
+

+ {mode === 'signin' ? "Pas de compte ?" : "Déjà membre ?"} + +

+
+ + +
+
+
+ ); +}; + +export default AuthPage; \ No newline at end of file diff --git a/components/BookSettings.tsx b/components/BookSettings.tsx new file mode 100644 index 0000000..49fcfc6 --- /dev/null +++ b/components/BookSettings.tsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState } from 'react'; +import { BookProject, BookSettings } from '../types'; +import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '../constants'; +import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react'; + +interface BookSettingsProps { + project: BookProject; + onUpdate: (project: BookProject) => void; +} + +const DEFAULT_SETTINGS: BookSettings = { + genre: '', + subGenre: '', + targetAudience: '', + tone: '', + pov: '', + tense: '', + synopsis: '', + themes: '' +}; + +const BookSettingsComponent: React.FC = ({ project, onUpdate }) => { + const [settings, setSettings] = useState(project.settings || DEFAULT_SETTINGS); + + useEffect(() => { + if (project.settings) { + setSettings(project.settings); + } + }, [project.settings]); + + const handleChange = (key: keyof BookSettings, value: string) => { + const newSettings = { ...settings, [key]: value }; + setSettings(newSettings); + onUpdate({ ...project, settings: newSettings }); + }; + + const handleStyleGuideChange = (value: string) => { + onUpdate({ ...project, styleGuide: value }); + }; + + return ( +
+
+ +
+
+ +
+
+

Paramètres Généraux du Roman

+

Définissez l'identité, le ton et les règles de votre œuvre pour guider l'IA.

+
+
+ +
+
+

+ Informations de Base +

+
+
+ + onUpdate({ ...project, title: e.target.value })} + className="w-full p-2.5 bg-[#eef2ff] border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-serif font-bold text-lg" + /> +
+
+ + onUpdate({ ...project, author: e.target.value })} + className="w-full p-2.5 bg-[#eef2ff] border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" + /> +
+
+
+ +