feat: implement core application structure, UI components, internationalization, and database seeding.

This commit is contained in:
2026-03-04 13:50:37 +01:00
parent 85642b4672
commit 5101f39ba0
49 changed files with 2732 additions and 980 deletions

38
src/app/cgu/page.tsx Normal file
View File

@@ -0,0 +1,38 @@
'use client';
import React from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book } from 'lucide-react';
import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
export default function CGUPage() {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</Link>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<Link href="/" className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={16} /> {t('common.back')}
</Link>
</div>
</nav>
<main className="max-w-4xl mx-auto py-20 px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgu_title')}</h1>
<div className="bg-white p-8 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6">
<p>{t('legal.cgu_content')}</p>
<p><i>(Ceci est un document type en attente de la version finale par un conseiller juridique)</i></p>
</div>
</main>
</div>
);
}

38
src/app/cgv/page.tsx Normal file
View File

@@ -0,0 +1,38 @@
'use client';
import React from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book } from 'lucide-react';
import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
export default function CGVPage() {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</Link>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<Link href="/" className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={16} /> {t('common.back')}
</Link>
</div>
</nav>
<main className="max-w-4xl mx-auto py-20 px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgv_title')}</h1>
<div className="bg-white p-8 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6">
<p>{t('legal.cgv_content')}</p>
<p><i>(Ceci est un document type en attente de la version finale par un conseiller juridique)</i></p>
</div>
</main>
</div>
);
}

View File

@@ -24,7 +24,7 @@ export default function DashboardPage() {
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
<div className="flex items-center gap-2">
<BookOpen className="text-blue-500" size={20} />
<span className="text-lg font-bold">PlumeIA</span>
<span className="text-lg font-bold">Pluume</span>
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter, Merriweather } from "next/font/google";
import { AuthProvider } from "@/providers/AuthProvider";
import { LanguageProvider } from "@/providers/LanguageProvider";
import "./globals.css";
const inter = Inter({
@@ -15,7 +16,7 @@ const merriweather = Merriweather({
});
export const metadata: Metadata = {
title: "PlumeIA - Éditeur Intelligent",
title: "Pluume - Éditeur Intelligent",
description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.",
};
@@ -25,10 +26,12 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-hidden antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
<html lang="en">
<body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
<AuthProvider>
{children}
<LanguageProvider>
{children}
</LanguageProvider>
</AuthProvider>
</body>
</html>

View File

@@ -62,7 +62,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
<div className="flex items-center gap-2">
<BookOpen className="text-blue-500" size={20} />
<span className="text-lg font-bold">PlumeIA</span>
<span className="text-lg font-bold">Pluume</span>
</div>
</div>
);

63
src/app/sitemap/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
'use client';
import React from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book, Link as LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
export default function SitemapPage() {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</Link>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<Link href="/" className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={16} /> {t('common.back')}
</Link>
</div>
</nav>
<main className="max-w-4xl mx-auto py-20 px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.sitemap_title')}</h1>
<div className="bg-white p-8 sm:p-12 rounded-3xl shadow-xl border border-indigo-50">
<ul className="space-y-4">
<li>
<Link href="/" className="flex items-center gap-3 text-lg font-bold text-slate-700 hover:text-blue-600 transition-colors">
<LinkIcon size={18} className="text-slate-400" /> Accueil
</Link>
</li>
<li>
<Link href="/auth" className="flex items-center gap-3 text-lg font-bold text-slate-700 hover:text-blue-600 transition-colors">
<LinkIcon size={18} className="text-slate-400" /> Authentification
</Link>
</li>
<li className="pt-4 mt-4 border-t border-slate-100">
<span className="text-xs font-black uppercase text-slate-400 tracking-widest block mb-4">Légal</span>
<ul className="space-y-4 pl-4">
<li>
<Link href="/cgu" className="flex items-center gap-3 text-base text-slate-600 hover:text-blue-600 transition-colors">
<LinkIcon size={16} className="text-slate-400" /> {t('legal.cgu_title')}
</Link>
</li>
<li>
<Link href="/cgv" className="flex items-center gap-3 text-base text-slate-600 hover:text-blue-600 transition-colors">
<LinkIcon size={16} className="text-slate-400" /> {t('legal.cgv_title')}
</Link>
</li>
</ul>
</li>
</ul>
</div>
</main>
</div>
);
}

View File

@@ -4,6 +4,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { Sparkles, Send, RefreshCw, BookOpen, Bot, ArrowLeft, BrainCircuit, Zap } from 'lucide-react';
import { ChatMessage, UserUsage } from '@/lib/types';
import { useLanguage } from '@/providers/LanguageProvider';
interface AIPanelProps {
chatHistory: ChatMessage[];
@@ -15,6 +16,7 @@ interface AIPanelProps {
}
const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertText, selectedText, isGenerating, usage }) => {
const { t } = useLanguage();
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -37,7 +39,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
<div className="p-4 bg-indigo-600 text-white flex items-center justify-between shadow-md">
<div className="flex items-center gap-2">
<Sparkles size={20} className="animate-pulse" />
<h3 className="font-bold tracking-tight">Assistant IA</h3>
<h3 className="font-bold tracking-tight">{t('ai_panel.title')}</h3>
</div>
{usage && (
<div className="bg-indigo-900/50 px-2 py-1 rounded text-[10px] font-black flex items-center gap-1">
@@ -48,7 +50,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{selectedText && (
<div className="bg-indigo-50 p-3 border-b border-indigo-100 text-xs text-indigo-800">
<div className="font-bold flex items-center gap-1 mb-1"><BookOpen size={12} /> Contexte :</div>
<div className="font-bold flex items-center gap-1 mb-1"><BookOpen size={12} /> {t('ai_panel.context')}</div>
<div className="italic truncate opacity-80">"{selectedText.substring(0, 60)}..."</div>
</div>
)}
@@ -57,10 +59,10 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{chatHistory.length === 0 && (
<div className="text-center text-theme-muted mt-10">
<Bot size={48} className="mx-auto mb-2 opacity-50" />
<p className="text-sm">Bonjour ! Comment puis-je vous aider aujourd'hui ?</p>
<p className="text-sm">{t('ai_panel.greeting')}</p>
{isLimitReached && (
<div className="mt-4 p-4 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs font-bold uppercase animate-pulse">
Limite atteinte ! Améliorez votre plan.
{t('ai_panel.limit_reached_upgrade')}
</div>
)}
</div>
@@ -70,7 +72,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
<div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
<div className={`max-w-[85%] rounded-2xl p-4 text-sm shadow-sm transition-colors duration-300 ${msg.role === 'user' ? 'bg-indigo-600 text-white rounded-br-none' : 'bg-theme-panel text-theme-text border border-theme-border rounded-bl-none'}`}>
{msg.role === 'model' && msg.responseType === 'reflection' && (
<div className="flex items-center gap-1.5 text-[10px] font-black text-amber-600 mb-1.5 uppercase tracking-wide"><BrainCircuit size={12} /> Réflexion</div>
<div className="flex items-center gap-1.5 text-[10px] font-black text-amber-600 mb-1.5 uppercase tracking-wide"><BrainCircuit size={12} /> {t('ai_panel.reflection')}</div>
)}
<div className="whitespace-pre-wrap leading-relaxed">{msg.text}</div>
</div>
@@ -80,7 +82,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{isGenerating && (
<div className="flex justify-start">
<div className="bg-theme-panel p-3 rounded-2xl rounded-bl-none shadow-sm border border-theme-border flex items-center gap-2 text-xs text-theme-muted transition-colors duration-300">
<RefreshCw size={14} className="animate-spin" /> L'IA travaille...
<RefreshCw size={14} className="animate-spin" /> {t('ai_panel.ai_working')}
</div>
</div>
)}
@@ -93,7 +95,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={isLimitReached ? "Limite atteinte..." : "Votre message..."}
placeholder={isLimitReached ? t('ai_panel.limit_reached') : t('ai_panel.your_message')}
className="w-full pl-4 pr-12 py-3 bg-theme-bg text-theme-text border border-theme-border rounded-2xl text-sm focus:outline-none focus:border-indigo-500 transition-all disabled:opacity-50"
disabled={isGenerating || isLimitReached}
/>

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react';
import { Mail, Lock, User, ArrowRight, Loader2, BookOpen, ShieldCheck } from 'lucide-react';
import { useAuthContext } from '@/providers/AuthProvider';
import { useLanguage } from '@/providers/LanguageProvider';
interface AuthPageProps {
onBack: () => void;
@@ -18,6 +19,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
// On récupère les fonctions de connexion directement du hook
const { user, login, signup } = useAuthContext();
const { t } = useLanguage();
// Redirection automatique dès que l'utilisateur est détecté dans l'état global
useEffect(() => {
@@ -28,7 +30,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
const handleAdminLogin = async () => {
const adminData = { email: 'streaper2@gmail.com', password: 'Kency1313' };
setFormData({ name: 'Admin Plume', ...adminData });
setFormData({ name: 'Admin Pluume', ...adminData });
setLoading(true);
setError('');
@@ -69,20 +71,20 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
</div>
<div className="relative z-10 flex items-center gap-2 text-white text-2xl font-black">
<BookOpen className="text-blue-500" /> PlumeIA
<BookOpen className="text-blue-500" /> Pluume
</div>
<div className="relative z-10 max-w-lg">
<h2 className="text-5xl font-black text-white leading-tight mb-6">
L'endroit où vos <span className="text-blue-400">histoires</span> prennent vie.
{t('auth.hero_title_part1')} <span className="text-blue-400">{t('auth.hero_title_part2')}</span> {t('auth.hero_title_part3')}
</h2>
<p className="text-slate-400 text-lg leading-relaxed">
Rejoignez une communauté d'auteurs qui utilisent l'IA pour briser la page blanche.
{t('auth.hero_desc')}
</p>
</div>
<div className="relative z-10 text-slate-500 text-sm">
© 2024 PlumeIA Ecosystem.
© 2024 Pluume Ecosystem.
</div>
</div>
@@ -91,10 +93,10 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
<div className="w-full max-w-md animate-in fade-in slide-in-from-right-10 duration-500 py-8">
<div className="text-center mb-10">
<h1 className="text-3xl font-black text-slate-900 mb-2">
{mode === 'signin' ? 'Content de vous revoir' : mode === 'signup' ? "Commencer l'aventure" : 'Récupération'}
{mode === 'signin' ? t('auth.welcome_back') : mode === 'signup' ? t('auth.start_adventure') : t('auth.recovery')}
</h1>
<p className="text-slate-500">
{mode === 'signin' ? 'Entrez vos identifiants pour continuer.' : 'Créez votre compte gratuit en quelques secondes.'}
{mode === 'signin' ? t('auth.enter_credentials') : t('auth.create_account_seconds')}
</p>
</div>
@@ -107,7 +109,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
<form onSubmit={handleSubmit} className="space-y-4">
{mode === 'signup' && (
<div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">Nom complet</label>
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">{t('auth.full_name')}</label>
<div className="relative">
<User className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input
@@ -115,7 +117,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Marc Dupré"
placeholder={t('auth.name_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"
/>
</div>
@@ -123,7 +125,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
)}
<div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">Email</label>
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">{t('auth.email')}</label>
<div className="relative">
<Mail className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input
@@ -131,7 +133,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="votre@email.com"
placeholder={t('auth.email_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"
/>
</div>
@@ -139,7 +141,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
{mode !== 'forgot' && (
<div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">Mot de passe</label>
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">{t('auth.password')}</label>
<div className="relative">
<Lock className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input
@@ -147,7 +149,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder=""
placeholder={t('auth.password_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"
/>
</div>
@@ -160,7 +162,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4"
>
{loading ? <Loader2 className="animate-spin" /> : (
<>{mode === 'signin' ? 'Se connecter' : mode === 'signup' ? 'Créer mon compte' : 'Envoyer'} <ArrowRight size={18} /></>
<>{mode === 'signin' ? t('auth.signin_button') : mode === 'signup' ? t('auth.signup_button') : t('auth.send_button')} <ArrowRight size={18} /></>
)}
</button>
</form>
@@ -170,24 +172,24 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
onClick={handleAdminLogin}
className="w-full mt-4 bg-amber-50 border border-amber-200 text-amber-800 py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-amber-100 transition-all"
>
<ShieldCheck size={18} /> Connexion démo (Admin)
<ShieldCheck size={18} /> {t('auth.demo_admin')}
</button>
)}
<div className="mt-10 text-center">
<p className="text-sm text-slate-500">
{mode === 'signin' ? "Pas de compte ?" : "Déjà membre ?"}
{mode === 'signin' ? t('auth.no_account') : t('auth.already_member')}
<button
onClick={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
className="ml-2 font-bold text-blue-600"
>
{mode === 'signin' ? "S'inscrire" : "Se connecter"}
{mode === 'signin' ? t('auth.signup_link') : t('auth.signin_link')}
</button>
</p>
</div>
<button onClick={onBack} className="mt-8 text-xs text-slate-300 w-full text-center hover:text-slate-500 transition-colors">
Revenir au site
{t('auth.back_to_site')}
</button>
</div>
</div>

View File

@@ -4,6 +4,8 @@ import React, { useEffect, useState } from 'react';
import { BookProject, BookSettings } from '@/lib/types';
import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants';
import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations';
interface BookSettingsProps {
project: BookProject;
@@ -23,6 +25,7 @@ const DEFAULT_SETTINGS: BookSettings = {
};
const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => {
const { t } = useLanguage();
const [settings, setSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -51,19 +54,19 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Settings size={24} />
</div>
<div>
<h2 className="text-2xl font-bold">Paramètres Généraux du Roman</h2>
<p className="text-slate-400 text-sm">Définissez l'identité, le ton et les règles de votre œuvre pour guider l'IA.</p>
<h2 className="text-2xl font-bold">{t('book_settings.title')}</h2>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p>
</div>
</div>
<div className="p-8 space-y-8">
<section className="space-y-4">
<h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2">
<Book size={18} className="text-blue-600" /> Informations de Base
<Book size={18} className="text-blue-600" /> {t('book_settings.basic_info')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Titre du Roman</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.novel_title')}</label>
<input
type="text"
value={project.title}
@@ -72,7 +75,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
/>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Nom d'Auteur</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.author_name')}</label>
<input
type="text"
value={project.author}
@@ -82,58 +85,58 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
</div>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Synopsis Global</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label>
<textarea
value={settings.synopsis}
onChange={(e) => handleChange('synopsis', e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none h-24 text-sm transition-colors duration-300"
placeholder="De quoi parle votre histoire dans les grandes lignes ?"
placeholder={t('book_settings.synopsis_placeholder')}
/>
</div>
</section>
<section className="space-y-4">
<h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2">
<Target size={18} className="text-red-500" /> Genre & Public
<Target size={18} className="text-red-500" /> {t('book_settings.genre_audience')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Genre Principal</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.main_genre')}</label>
<input
type="text"
list="genre-suggestions"
value={settings.genre}
onChange={(e) => handleChange('genre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Fantasy"
placeholder={t('book_settings.genre_placeholder')}
/>
<datalist id="genre-suggestions">
{GENRES.map(g => <option key={g} value={g} />)}
</datalist>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Sous-Genre</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.sub_genre')}</label>
<input
type="text"
value={settings.subGenre || ''}
onChange={(e) => handleChange('subGenre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Dark Fantasy"
placeholder={t('book_settings.subgenre_placeholder')}
/>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Public Cible</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.target_audience')}</label>
<input
type="text"
value={settings.targetAudience}
onChange={(e) => handleChange('targetAudience', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Jeune Adulte, Adulte..."
placeholder={t('book_settings.audience_placeholder')}
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Thèmes Clés</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.key_themes')}</label>
<div className="relative">
<Hash size={14} className="absolute left-3 top-3 text-theme-muted" />
<input
@@ -141,7 +144,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
value={settings.themes}
onChange={(e) => handleChange('themes', e.target.value)}
className="w-full pl-9 p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Vengeance, Rédemption, Voyage initiatique..."
placeholder={t('book_settings.themes_placeholder')}
/>
</div>
</div>
@@ -149,90 +152,90 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<section className="space-y-4">
<h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2">
<Feather size={18} className="text-purple-600" /> Narration & Style
<Feather size={18} className="text-purple-600" /> {t('book_settings.narration_style')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1 flex items-center gap-1">
<Users size={14} /> Point de Vue (POV)
<Users size={14} /> {t('book_settings.pov')}
</label>
<select
value={settings.pov}
onChange={(e) => handleChange('pov', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
>
<option value="">Sélectionner...</option>
{POV_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)}
<option value="">{t('book_settings.select')}</option>
{POV_OPTIONS.map(o => <option key={o} value={o}>{t(`pov_options.${o.toLowerCase().replace(/\s+/g, '_')}` as TranslationKey) || o}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1 flex items-center gap-1">
<Clock size={14} /> Temps du récit
<Clock size={14} /> {t('book_settings.tense')}
</label>
<select
value={settings.tense}
onChange={(e) => handleChange('tense', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
>
<option value="">Sélectionner...</option>
{TENSE_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)}
<option value="">{t('book_settings.select')}</option>
{TENSE_OPTIONS.map(o => <option key={o} value={o}>{t(`tense_options.${o.toLowerCase().replace(/\s+/g, '_')}` as TranslationKey) || o}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Ton Général</label>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.general_tone')}</label>
<input
type="text"
list="tone-suggestions"
value={settings.tone}
onChange={(e) => handleChange('tone', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Sombre, Ironique..."
placeholder={t('book_settings.tone_placeholder')}
/>
<datalist id="tone-suggestions">
{TONES.map(t => <option key={t} value={t} />)}
{TONES.map(tOption => <option key={tOption} value={tOption} />)}
</datalist>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-semibold text-theme-muted mb-1">
Guide de Style & Instructions IA (Prompt Système)
{t('book_settings.style_guide')}
</label>
<p className="text-xs text-theme-muted mb-2">
Ces instructions seront envoyées à l'IA à chaque génération. Décrivez ici le style d'écriture désiré (ex: "phrases courtes", "vocabulaire soutenu", "beaucoup de métaphores").
{t('book_settings.style_guide_help')}
</p>
<textarea
value={project.styleGuide || ''}
onChange={(e) => handleStyleGuideChange(e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 text-sm font-mono transition-colors duration-300"
placeholder="Ex: Utilise un style descriptif et sensoriel. Évite les adverbes. Le narrateur est cynique."
placeholder={t('book_settings.style_guide_placeholder')}
/>
</div>
</section>
<section className="space-y-4 pt-8 border-t border-red-200">
<h3 className="text-lg font-bold text-red-600 flex items-center gap-2 pb-2">
<span className="bg-red-100 p-1 rounded"></span> Zone de Danger
<span className="bg-red-100 p-1 rounded"></span> {t('book_settings.danger_zone')}
</h3>
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h4 className="font-bold text-red-900 mb-2">Supprimer le projet</h4>
<h4 className="font-bold text-red-900 mb-2">{t('book_settings.delete_project')}</h4>
<p className="text-sm text-red-700 mb-4">
Cette action est irréversible. Toutes les données associées à ce projet (chapitres, entités, idées) seront définitivement effacées.
{t('book_settings.delete_warning')}
</p>
{showDeleteConfirm ? (
<div className="flex items-center gap-4 bg-theme-panel p-4 rounded border border-red-200">
<span className="text-sm font-bold text-theme-text">Êtes-vous sûr ?</span>
<span className="text-sm font-bold text-theme-text">{t('book_settings.are_you_sure')}</span>
<button
onClick={onDeleteProject}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm font-bold opacity-90 transition-opacity"
>
Oui, supprimer définitivement
{t('book_settings.confirm_delete')}
</button>
<button
onClick={() => setShowDeleteConfirm(false)}
className="px-4 py-2 bg-theme-bg text-theme-text border border-theme-border rounded hover:opacity-80 text-sm transition-opacity"
>
Annuler
{t('book_settings.cancel')}
</button>
</div>
) : (
@@ -240,7 +243,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
onClick={() => setShowDeleteConfirm(true)}
className="px-4 py-2 bg-theme-panel border border-red-300 text-red-600 rounded hover:bg-red-50 text-sm font-bold transition-colors duration-300"
>
Supprimer ce projet
{t('book_settings.delete_button')}
</button>
)}
</div>

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { CreditCard, Shield, Lock, ArrowRight, Loader2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface CheckoutProps {
onComplete: () => void;
@@ -11,12 +12,13 @@ interface CheckoutProps {
const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
const [loading, setLoading] = useState(false);
const { t } = useLanguage();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setTimeout(() => {
onComplete();
onComplete();
}, 2000);
};
@@ -24,38 +26,38 @@ const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
<div className="min-h-screen bg-[#eef2ff] flex items-center justify-center p-8">
<div className="bg-white rounded-3xl shadow-2xl flex flex-col md:flex-row max-w-4xl w-full overflow-hidden animate-in fade-in slide-in-from-bottom-10 duration-500">
<div className="w-full md:w-1/3 bg-slate-900 text-white p-8">
<h3 className="text-xl font-bold mb-8 flex items-center gap-2"><Lock size={18} className="text-blue-400" /> Commande</h3>
<div className="space-y-4">
<div className="flex justify-between text-sm"><span>Auteur Pro</span><span>12.00</span></div>
<div className="flex justify-between text-sm"><span>TVA (20%)</span><span>2.40</span></div>
<div className="h-px bg-slate-800 my-4" />
<div className="flex justify-between text-xl font-black"><span>Total</span><span className="text-blue-400">14.40</span></div>
</div>
<h3 className="text-xl font-bold mb-8 flex items-center gap-2"><Lock size={18} className="text-blue-400" /> {t('checkout.order')}</h3>
<div className="space-y-4">
<div className="flex justify-between text-sm"><span>{t('checkout.pro_author')}</span><span>12.00</span></div>
<div className="flex justify-between text-sm"><span>{t('checkout.vat')}</span><span>2.40</span></div>
<div className="h-px bg-slate-800 my-4" />
<div className="flex justify-between text-xl font-black"><span>{t('checkout.total')}</span><span className="text-blue-400">14.40</span></div>
</div>
</div>
<div className="flex-1 p-8 md:p-12">
<h2 className="text-2xl font-black text-slate-900 mb-8 text-center">Paiement Sécurisé</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-xs font-black text-slate-500 uppercase tracking-widest mb-2">Numéro de carte</label>
<div className="relative">
<input type="text" placeholder="4242 4242 4242 4242" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
<CreditCard className="absolute right-4 top-4 text-slate-400" />
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<input type="text" placeholder="MM / YY" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
<input type="text" placeholder="CVC" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
</div>
<button
disabled={loading}
className="w-full bg-blue-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center justify-center gap-3"
>
{loading ? <Loader2 className="animate-spin" /> : <>Confirmer le paiement <ArrowRight size={20} /></>}
</button>
<div className="flex items-center justify-center gap-2 text-[10px] text-slate-400 font-bold uppercase">
<Shield size={12} /> Traitement chiffré SSL 256-bits
</div>
</form>
<h2 className="text-2xl font-black text-slate-900 mb-8 text-center">{t('checkout.secure_payment')}</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-xs font-black text-slate-500 uppercase tracking-widest mb-2">{t('checkout.card_number')}</label>
<div className="relative">
<input type="text" placeholder="4242 4242 4242 4242" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
<CreditCard className="absolute right-4 top-4 text-slate-400" />
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<input type="text" placeholder="MM / YY" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
<input type="text" placeholder="CVC" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
</div>
<button
disabled={loading}
className="w-full bg-blue-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center justify-center gap-3"
>
{loading ? <Loader2 className="animate-spin" /> : <>{t('checkout.confirm_payment')} <ArrowRight size={20} /></>}
</button>
<div className="flex items-center justify-center gap-2 text-[10px] text-slate-400 font-bold uppercase">
<Shield size={12} /> {t('checkout.ssl_encryption')}
</div>
</form>
</div>
</div>
</div>

View File

@@ -4,6 +4,8 @@
import React from 'react';
import { BookProject, UserProfile } from '@/lib/types';
import { Plus, Book, Clock, Star, ChevronRight, LogOut, LayoutDashboard, User, Target, Flame, Edit3 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface DashboardProps {
user: UserProfile;
@@ -16,6 +18,8 @@ interface DashboardProps {
}
const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreate, onLogout, onPricing, onProfile }) => {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-theme-bg p-8 font-sans transition-colors duration-300">
<div className="max-w-6xl mx-auto space-y-8">
@@ -28,18 +32,19 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="absolute -bottom-1 -right-1 bg-green-500 w-5 h-5 rounded-full border-4 border-white" />
</div>
<div>
<h2 className="text-3xl font-black text-theme-text">Bonjour, {user.name} 👋</h2>
<h2 className="text-3xl font-black text-theme-text">{t('dashboard.hello')}, {user.name} 👋</h2>
<div className="flex items-center gap-3 mt-1">
<span className="px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-[10px] uppercase font-black tracking-widest">{user.subscription.planDetails?.displayName || user.subscription.plan}</span>
<span className="text-theme-muted text-xs font-medium">Membre depuis le 24 janv.</span>
<span className="text-theme-muted text-xs font-medium">{t('dashboard.member_since')} 24 janv.</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<LanguageSwitcher />
<button onClick={onProfile} className="bg-theme-bg text-theme-text px-5 py-2.5 rounded-xl text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border">
<User size={18} /> Mon Profil
<User size={18} /> {t('dashboard.my_profile')}
</button>
<button onClick={onLogout} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button>
<button onClick={onLogout} title={t('sidebar.logout')} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button>
</div>
</div>
@@ -48,22 +53,22 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="bg-theme-panel p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4">
<div className="bg-orange-100 p-3 rounded-2xl text-orange-600"><Flame size={24} /></div>
<div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Série actuelle</p>
<p className="text-2xl font-black text-theme-text">{user.stats.writingStreak} Jours</p>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.streak')}</p>
<p className="text-2xl font-black text-theme-text">{user.stats.writingStreak} {t('dashboard.days')}</p>
</div>
</div>
<div className="bg-theme-panel p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4">
<div className="bg-blue-100 p-3 rounded-2xl text-blue-600"><Edit3 size={24} /></div>
<div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Mots écrits</p>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.words_written')}</p>
<p className="text-2xl font-black text-theme-text">{user.stats.totalWordsWritten.toLocaleString()}</p>
</div>
</div>
<div className="bg-theme-panel p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4">
<div className="bg-indigo-100 p-3 rounded-2xl text-indigo-600"><Target size={24} /></div>
<div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Objectif du jour</p>
<p className="text-2xl font-black text-theme-text">{user.preferences.dailyWordGoal} Mots</p>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.daily_goal')}</p>
<p className="text-2xl font-black text-theme-text">{user.preferences.dailyWordGoal} {t('dashboard.words')}</p>
</div>
</div>
</div>
@@ -72,12 +77,12 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{/* Project List */}
<div className="lg:col-span-2 space-y-4">
<div className="flex justify-between items-center mb-6">
<h3 className="text-2xl font-black text-theme-text">Mes Romans</h3>
<h3 className="text-2xl font-black text-theme-text">{t('dashboard.my_novels')}</h3>
<button
onClick={onCreate}
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200"
>
<Plus size={20} /> Écrire un nouveau livre
<Plus size={20} /> {t('dashboard.write_new')}
</button>
</div>
@@ -93,10 +98,10 @@ 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">Dernière modification : {new Date(p.lastModified).toLocaleDateString()}</p>
<p className="text-theme-muted text-sm">{t('dashboard.last_modified')} : {new Date(p.lastModified).toLocaleDateString()}</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} Chapitres</span>
<span>{p.chapters.length} {t('nav.chapters')}</span>
<ChevronRight size={20} className="group-hover:text-blue-600 transition-transform group-hover:translate-x-1 duration-300" />
</div>
</div>
@@ -104,8 +109,8 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{projects.length === 0 && (
<div className="col-span-2 py-24 bg-theme-panel rounded-[3rem] border-2 border-dashed border-theme-border flex flex-col items-center justify-center text-theme-muted">
<Book size={64} className="mb-6 opacity-20" />
<p className="font-bold text-lg">Prêt à commencer votre premier roman ?</p>
<button onClick={onCreate} className="mt-4 text-blue-600 font-bold hover:underline">Créer un projet maintenant</button>
<p className="font-bold text-lg">{t('dashboard.empty_projects')}</p>
<button onClick={onCreate} className="mt-4 text-blue-600 font-bold hover:underline">{t('dashboard.create_now')}</button>
</div>
)}
</div>
@@ -115,11 +120,11 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="space-y-6">
<div className="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/20 blur-[60px] -z-1" />
<h3 className="font-black text-xl mb-6 flex items-center gap-2"><Star size={20} className="text-yellow-400" /> Utilisation</h3>
<h3 className="font-black text-xl mb-6 flex items-center gap-2"><Star size={20} className="text-yellow-400" /> {t('dashboard.usage')}</h3>
<div className="space-y-8">
<div>
<div className="flex justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">
<span>Actions IA</span>
<span>{t('sidebar.ai_actions')}</span>
<span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span>
</div>
<div className="h-3 w-full bg-slate-800 rounded-full overflow-hidden">
@@ -131,7 +136,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div>
<div>
<div className="flex justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">
<span>Emplacements Roman</span>
<span>{t('dashboard.novel_slots')}</span>
<span>{projects.length} / {user.usage.projectsLimit}</span>
</div>
<div className="h-3 w-full bg-slate-800 rounded-full overflow-hidden">
@@ -143,7 +148,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div>
</div>
<button onClick={onPricing} className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all">
Upgrade Plan
{t('dashboard.upgrade_plan')}
</button>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { BookProject } from '@/lib/types';
import { FileText, FileType, Printer, X, Download, Book, FileJson } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface ExportModalProps {
isOpen: boolean;
@@ -19,6 +20,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
const [pageSize, setPageSize] = useState<PageSize>('A4');
const [includeCover, setIncludeCover] = useState(true);
const [includeTOC, setIncludeTOC] = useState(true);
const { t } = useLanguage();
if (!isOpen) return null;
@@ -139,7 +141,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
<div className="bg-slate-900 text-white p-6 flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Download size={24} /> Exporter le livre
<Download size={24} /> {t('export.title')}
</h2>
<p className="text-slate-400 text-sm mt-1">{project.title}</p>
</div>
@@ -158,7 +160,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'pdf' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
>
<Printer size={32} />
<div className="font-semibold">PDF (Impression)</div>
<div className="font-semibold">{t('export.pdf_format')}</div>
</button>
<button
@@ -166,7 +168,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'word' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
>
<FileText size={32} />
<div className="font-semibold">Microsoft Word</div>
<div className="font-semibold">{t('export.word_format')}</div>
</button>
<button
@@ -174,7 +176,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'epub' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
>
<Book size={32} />
<div className="font-semibold">EPUB / Ebook</div>
<div className="font-semibold">{t('export.epub_format')}</div>
</button>
<button
@@ -182,29 +184,29 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'markdown' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
>
<FileJson size={32} />
<div className="font-semibold">Markdown</div>
<div className="font-semibold">{t('export.markdown_format')}</div>
</button>
</div>
{/* Options Section */}
<div className="bg-slate-50 rounded-lg p-5 border border-slate-200">
<h3 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">
Paramètres d'exportation ({format.toUpperCase()})
{t('export.settings')} ({format.toUpperCase()})
</h3>
<div className="space-y-4">
{format === 'pdf' && (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<label className="text-slate-700 font-medium">Format du papier</label>
<span className="text-xs text-slate-400">Géré par l'imprimante (A4, A5...)</span>
<label className="text-slate-700 font-medium">{t('export.paper_format')}</label>
<span className="text-xs text-slate-400">{t('export.printer_managed')}</span>
</div>
<div className="bg-slate-200 px-3 py-1 rounded text-xs font-mono text-slate-600">Auto</div>
<div className="bg-slate-200 px-3 py-1 rounded text-xs font-mono text-slate-600">{t('export.auto')}</div>
</div>
)}
<div className="flex items-center justify-between">
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="cover">Inclure la page de titre</label>
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="cover">{t('export.include_cover')}</label>
<input
id="cover"
type="checkbox"
@@ -215,7 +217,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
</div>
<div className="flex items-center justify-between">
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="toc">Générer la table des matières</label>
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="toc">{t('export.generate_toc')}</label>
<input
id="toc"
type="checkbox"
@@ -227,7 +229,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
{format === 'epub' && (
<p className="text-xs text-amber-600 bg-amber-50 p-2 rounded mt-2">
Note: L'export EPUB génère un fichier XHTML optimisé prêt à être converti par Calibre ou Kindle Previewer.
{t('export.epub_note')}
</p>
)}
</div>
@@ -240,14 +242,14 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
onClick={onClose}
className="px-5 py-2 text-slate-600 hover:bg-slate-200 rounded-lg font-medium transition-colors"
>
Annuler
{t('export.cancel')}
</button>
<button
onClick={handleExport}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium shadow-md transition-all flex items-center gap-2"
>
{format === 'pdf' ? <Printer size={18} /> : <Download size={18} />}
{format === 'pdf' ? 'Imprimer / Enregistrer PDF' : `Télécharger .${format === 'word' ? 'doc' : format === 'epub' ? 'xhtml' : 'md'}`}
{format === 'pdf' ? t('export.print_save_pdf') : `${t('export.download')} .${format === 'word' ? 'doc' : format === 'epub' ? 'xhtml' : 'md'}`}
</button>
</div>
</div>

View File

@@ -2,41 +2,71 @@
import React from 'react';
import { Sparkles, Feather, Globe, GitGraph, BookOpen, Download, Lightbulb, Zap, ArrowLeft } from 'lucide-react';
import { ArrowLeft, BookOpen, Brain, Globe, ShieldCheck, Zap, Sparkles, LayoutDashboard, History, MessageSquare, Save, Users, Layers, Workflow, CheckCircle2, GitGraph, Lightbulb, Feather } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface FeaturesPageProps {
onBack: () => void;
}
const FeaturesPage: React.FC<FeaturesPageProps> = ({ onBack }) => {
const { t } = useLanguage();
const features = [
{ title: "Assistant IA Co-Auteur", icon: Sparkles, desc: "Générez des paragraphes, brainstormez des idées et demandez conseil à une IA qui connaît votre univers." },
{ title: "Bible du Monde Vivante", icon: Globe, desc: "Gérez vos personnages, lieux et objets. L'IA les reconnaît et garde une cohérence absolue." },
{ title: "Story Workflow", icon: GitGraph, desc: "Visualisez votre intrigue sous forme de nœuds et gérez les embranchements de votre récit." },
{ title: "Boîte à Idées Kanban", icon: Lightbulb, desc: "Notez vos idées fugaces et transformez-les en chapitres quand vous êtes prêt." },
{ title: "Mise en page Pro", icon: BookOpen, desc: "Exportez au format PDF, Word ou EPUB avec une mise en page soignée et automatique." },
{ title: "Éditeur Riche", icon: Feather, desc: "Un traitement de texte complet avec mode focus et historique des modifications IA." }
{ title: t('features.feat1_title'), icon: Sparkles, desc: t('features.feat1_desc') },
{ title: t('features.feat2_title'), icon: Globe, desc: t('features.feat2_desc') },
{ title: t('features.feat3_title'), icon: GitGraph, desc: t('features.feat3_desc') },
{ title: t('features.feat4_title'), icon: Lightbulb, desc: t('features.feat4_desc') },
{ title: t('features.feat5_title'), icon: BookOpen, desc: t('features.feat5_desc') },
{ title: t('features.feat6_title'), icon: Feather, desc: t('features.feat6_desc') }
];
return (
<div className="min-h-screen bg-[#eef2ff] py-20 px-8">
<div className="max-w-7xl mx-auto">
<button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 mb-12 font-bold transition-colors">
<ArrowLeft size={20} /> Retour
</button>
<h1 className="text-5xl font-black text-slate-900 mb-12 text-center">Un univers d'outils pour votre créativité.</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((f, i) => (
<div key={i} className="bg-white p-8 rounded-3xl shadow-xl border border-indigo-50 hover:scale-105 transition-transform">
<div className="w-12 h-12 bg-indigo-100 rounded-2xl flex items-center justify-center text-indigo-600 mb-6">
<f.icon size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{f.title}</h3>
<p className="text-slate-600 leading-relaxed">{f.desc}</p>
</div>
))}
<div className="min-h-screen bg-[#eef2ff] font-sans">
{/* Header */}
<div className="bg-slate-900 text-white pt-20 pb-32 px-8 relative overflow-hidden">
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-500/20 blur-[100px] rounded-full" />
<div className="max-w-7xl mx-auto relative z-10">
<div className="flex justify-between items-center mb-12">
<button onClick={onBack} className="flex items-center gap-2 text-slate-400 hover:text-white font-bold transition-colors">
<ArrowLeft size={20} /> {t('common.back')}
</button>
<LanguageSwitcher />
</div>
<h1 className="text-5xl font-black text-white mb-4 text-center">{t('features.title')}</h1>
<p className="text-slate-300 text-xl text-center max-w-3xl mx-auto">
{t('features.subtitle')}
</p>
</div>
</div>
{/* Features Grid */}
<div className="max-w-7xl mx-auto px-8 -mt-20 relative z-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((f, i) => (
<div key={i} className="bg-white p-8 rounded-3xl shadow-xl border border-indigo-50 hover:scale-105 transition-transform">
<div className="w-12 h-12 bg-indigo-100 rounded-2xl flex items-center justify-center text-indigo-600 mb-6">
<f.icon size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{f.title}</h3>
<p className="text-slate-600 leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-8 mt-20 text-center relative z-20">
<div className="max-w-7xl mx-auto">
<div className="flex flex-wrap items-center justify-center gap-6 mb-8 text-sm">
<Link href="/cgu" className="hover:text-white transition-colors">{t('footer.cgu')}</Link>
<Link href="/cgv" className="hover:text-white transition-colors">{t('footer.cgv')}</Link>
<Link href="/sitemap" className="hover:text-white transition-colors">{t('footer.sitemap')}</Link>
</div>
<p className="text-sm">{t('landing.copyright')}</p>
</div>
</footer>
</div>
);
};

View File

@@ -3,147 +3,150 @@
import React from 'react';
import { X, Keyboard, MousePointerClick, MessageCircle, Sparkles, GitGraph, BookOpen, Command, Globe, Layout, Settings, Lightbulb } from 'lucide-react';
import { ViewMode } from '@/lib/types';
import { useLanguage } from '@/providers/LanguageProvider';
interface HelpModalProps {
isOpen: boolean;
onClose: () => void;
viewMode: ViewMode;
isOpen: boolean;
onClose: () => void;
viewMode: ViewMode;
}
const Kbd: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<kbd className="px-2 py-1 text-xs font-semibold text-slate-800 bg-slate-100 border border-slate-300 rounded-md shadow-[0px_2px_0px_0px_rgba(203,213,225,1)] mx-1 font-mono">
{children}
</kbd>
<kbd className="px-2 py-1 text-xs font-semibold text-slate-800 bg-slate-100 border border-slate-300 rounded-md shadow-[0px_2px_0px_0px_rgba(203,213,225,1)] mx-1 font-mono">
{children}
</kbd>
);
const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
if (!isOpen) return null;
const { t } = useLanguage();
const renderContent = () => {
switch (viewMode) {
case 'ideas':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-yellow-600 flex items-center gap-2 border-b border-yellow-100 pb-2 mb-4">
<Lightbulb size={20} /> Boîte à Idées & Tâches
</h3>
<div className="text-sm text-slate-600 space-y-4">
<p>
Un espace de type Kanban pour ne rien oublier. Utilisez-le pour noter des idées fugaces, planifier des recherches ou lister les scènes à écrire.
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Glisser-Déposer :</span> Déplacez les cartes d'une colonne à l'autre (À faire En cours Validé) pour suivre votre progression.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Catégories :</span> Utilisez les catégories (Intrigue, Personnage, Recherche) pour filtrer visuellement vos tâches grâce aux codes couleurs.
</span>
</li>
</ul>
</div>
</section>
);
if (!isOpen) return null;
case 'workflow':
return (
<>
{/* Workflow Section */}
const renderContent = () => {
switch (viewMode) {
case 'ideas':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-indigo-700 flex items-center gap-2 border-b border-indigo-100 pb-2 mb-4">
<GitGraph size={20} /> Organisation Narrative
<h3 className="text-lg font-bold text-yellow-600 flex items-center gap-2 border-b border-yellow-100 pb-2 mb-4">
<Lightbulb size={20} /> Boîte à Idées & Tâches
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-slate-600">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Sélection :</span> <Kbd>Ctrl</Kbd> + Clic pour sélectionner plusieurs cartes. Glissez pour déplacer tout un groupe.
</span>
</li>
<li className="flex items-start gap-2">
<Command size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Copier / Coller :</span> <Kbd>Ctrl</Kbd> + <Kbd>C</Kbd> pour copier les nœuds sélectionnés, <Kbd>Ctrl</Kbd> + <Kbd>V</Kbd> pour coller.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Connexions :</span> Tirez depuis le cercle à droite d'une carte pour lier les événements.
</span>
</li>
</ul>
<div className="text-sm text-slate-600 space-y-4">
<p>
Un espace de type Kanban pour ne rien oublier. Utilisez-le pour noter des idées fugaces, planifier des recherches ou lister les scènes à écrire.
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Glisser-Déposer :</span> Déplacez les cartes d'une colonne à l'autre (À faire En cours Validé) pour suivre votre progression.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Catégories :</span> Utilisez les catégories (Intrigue, Personnage, Recherche) pour filtrer visuellement vos tâches grâce aux codes couleurs.
</span>
</li>
</ul>
</div>
</section>
);
{/* Dialogue Intelligent */}
<section className="bg-blue-50 p-6 rounded-xl border border-blue-100 mb-8">
<h3 className="text-lg font-bold text-blue-800 flex items-center gap-2 border-b border-blue-200 pb-2 mb-4">
<MessageCircle size={20} /> Mode Dialogue (Workflow)
case 'workflow':
return (
<>
{/* Workflow Section */}
<section className="mb-8">
<h3 className="text-lg font-bold text-indigo-700 flex items-center gap-2 border-b border-indigo-100 pb-2 mb-4">
<GitGraph size={20} /> Organisation Narrative
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-slate-600">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Sélection :</span> <Kbd>Ctrl</Kbd> + Clic pour sélectionner plusieurs cartes. Glissez pour déplacer tout un groupe.
</span>
</li>
<li className="flex items-start gap-2">
<Command size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Copier / Coller :</span> <Kbd>Ctrl</Kbd> + <Kbd>C</Kbd> pour copier les nœuds sélectionnés, <Kbd>Ctrl</Kbd> + <Kbd>V</Kbd> pour coller.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Connexions :</span> Tirez depuis le cercle à droite d'une carte pour lier les événements.
</span>
</li>
</ul>
</div>
</section>
{/* Dialogue Intelligent */}
<section className="bg-blue-50 p-6 rounded-xl border border-blue-100 mb-8">
<h3 className="text-lg font-bold text-blue-800 flex items-center gap-2 border-b border-blue-200 pb-2 mb-4">
<MessageCircle size={20} /> Mode Dialogue (Workflow)
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
<div>
<div className="font-semibold text-slate-800 mb-1">Écriture Rapide</div>
<p className="text-slate-600 leading-relaxed mb-3">
Tapez un nom et <Kbd>Entrée</Kbd> : le formatage <code>Nom: </code> s'ajoute seul.
</p>
<p className="text-slate-600 leading-relaxed">
Dans un dialogue, <Kbd>Entrée</Kbd> change de ligne et <strong>devine le prochain interlocuteur</strong> automatiquement.
</p>
</div>
<div>
<div className="font-semibold text-slate-800 mb-1">Rotation & Insertion</div>
<p className="text-slate-600 leading-relaxed mb-3">
<Kbd>Tab</Kbd> permute instantanément entre les personnages présents dans la scène.
</p>
<p className="text-slate-600 leading-relaxed">
Utilisez <Kbd>@</Kbd> pour insérer un personnage, <Kbd>#</Kbd> pour un lieu.
</p>
</div>
</div>
</section>
</>
);
case 'world_building':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-green-700 flex items-center gap-2 border-b border-green-100 pb-2 mb-4">
<Globe size={20} /> Bible du Monde
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
<div>
<div className="font-semibold text-slate-800 mb-1">Écriture Rapide</div>
<p className="text-slate-600 leading-relaxed mb-3">
Tapez un nom et <Kbd>Entrée</Kbd> : le formatage <code>Nom: </code> s'ajoute seul.
<div className="text-sm text-slate-600 space-y-4">
<p>
La bible du monde permet de centraliser toutes les informations sur vos personnages et lieux.
Ces informations sont <strong>lues par l'IA</strong> pour assurer la cohérence de l'histoire.
</p>
<p className="text-slate-600 leading-relaxed">
Dans un dialogue, <Kbd>Entrée</Kbd> change de ligne et <strong>devine le prochain interlocuteur</strong> automatiquement.
</p>
</div>
<div>
<div className="font-semibold text-slate-800 mb-1">Rotation & Insertion</div>
<p className="text-slate-600 leading-relaxed mb-3">
<Kbd>Tab</Kbd> permute instantanément entre les personnages présents dans la scène.
</p>
<p className="text-slate-600 leading-relaxed">
Utilisez <Kbd>@</Kbd> pour insérer un personnage, <Kbd>#</Kbd> pour un lieu.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Modèles Personnalisés</h4>
<p>
Cliquez sur le bouton "Modèles" pour ajouter des champs spécifiques (ex: "Type de Magie", "Allégeance") à tous vos personnages ou lieux.
</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Contexte Automatique</h4>
<p>
Le champ "Contexte Narratif" se remplit automatiquement au fur et à mesure que vous écrivez votre histoire et que l'IA détecte l'évolution des personnages.
</p>
</div>
</div>
</div>
</section>
</>
);
case 'world_building':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-green-700 flex items-center gap-2 border-b border-green-100 pb-2 mb-4">
<Globe size={20} /> Bible du Monde
</h3>
<div className="text-sm text-slate-600 space-y-4">
<p>
La bible du monde permet de centraliser toutes les informations sur vos personnages et lieux.
Ces informations sont <strong>lues par l'IA</strong> pour assurer la cohérence de l'histoire.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Modèles Personnalisés</h4>
<p>
Cliquez sur le bouton "Modèles" pour ajouter des champs spécifiques (ex: "Type de Magie", "Allégeance") à tous vos personnages ou lieux.
</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Contexte Automatique</h4>
<p>
Le champ "Contexte Narratif" se remplit automatiquement au fur et à mesure que vous écrivez votre histoire et que l'IA détecte l'évolution des personnages.
</p>
</div>
</div>
</div>
</section>
);
);
case 'settings':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-slate-700 flex items-center gap-2 border-b border-slate-100 pb-2 mb-4">
<Settings size={20} /> Paramètres du Livre
<Settings size={20} /> Paramètres du Livre
</h3>
<p className="text-sm text-slate-600 mb-4">
Ces réglages sont cruciaux pour l'Assistant IA. Ils définissent le "ton" de toutes les générations de texte.
@@ -155,131 +158,131 @@ const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
</section>
);
case 'write':
default:
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-amber-600 flex items-center gap-2 border-b border-amber-100 pb-2 mb-4">
<Sparkles size={20} /> Éditeur & Assistant IA
</h3>
<div className="space-y-4 text-sm text-slate-600">
<div className="bg-amber-50 p-4 rounded-lg border border-amber-100">
<h4 className="font-bold text-amber-800 mb-2">Menu Contextuel Intelligent</h4>
<p>Sélectionnez du texte et faites un <strong>clic droit</strong> pour :</p>
<ul className="grid grid-cols-2 gap-2 mt-2 pl-4">
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Corriger l'orthographe</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Reformuler / Améliorer</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Développer (Show, don't tell)</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Continuer l'écriture</li>
</ul>
case 'write':
default:
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-amber-600 flex items-center gap-2 border-b border-amber-100 pb-2 mb-4">
<Sparkles size={20} /> Éditeur & Assistant IA
</h3>
<div className="space-y-4 text-sm text-slate-600">
<div className="bg-amber-50 p-4 rounded-lg border border-amber-100">
<h4 className="font-bold text-amber-800 mb-2">Menu Contextuel Intelligent</h4>
<p>Sélectionnez du texte et faites un <strong>clic droit</strong> pour :</p>
<ul className="grid grid-cols-2 gap-2 mt-2 pl-4">
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Corriger l'orthographe</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Reformuler / Améliorer</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Développer (Show, don't tell)</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Continuer l'écriture</li>
</ul>
</div>
<p>
<span className="font-semibold text-slate-800">Historique des versions :</span> Activez la marge de droite (icône horloge) pour voir toutes les interventions de l'IA et revenir en arrière si nécessaire.
</p>
<p>
<span className="font-semibold text-slate-800">Chat Latéral :</span> Posez des questions sur votre histoire, demandez des résumés ou des idées de rebondissements. L'IA connaît le contexte de vos chapitres précédents et de vos fiches personnages.
</p>
<div className="mt-6 border-t border-slate-100 pt-4">
<h4 className="font-bold text-slate-700 mb-3 flex items-center gap-2">
<Keyboard size={16} /> Raccourcis Clavier (Éditeur)
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg border border-slate-100">
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Gras</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>B</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Italique</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>I</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Souligner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>U</Kbd></span>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Tout sélectionner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>A</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Annuler</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Z</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Rétablir</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Shift</Kbd> + <Kbd>Z</Kbd></span>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white rounded-xl shadow-2xl w-[800px] max-h-[90vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-slate-900 text-white p-6 flex justify-between items-center shrink-0">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<BookOpen size={24} className="text-blue-400" /> {t('help.title')} : {
viewMode === 'workflow' ? t('help.workflow_title_doc') :
viewMode === 'world_building' ? t('help.world_building_title') :
viewMode === 'settings' ? t('help.settings_title') :
viewMode === 'ideas' ? t('help.ideas_title') :
t('help.editor_ai_title')
}
</h2>
<p className="text-slate-400 text-sm mt-1">{t('help.subtitle')}</p>
</div>
<p>
<span className="font-semibold text-slate-800">Historique des versions :</span> Activez la marge de droite (icône horloge) pour voir toutes les interventions de l'IA et revenir en arrière si nécessaire.
</p>
<p>
<span className="font-semibold text-slate-800">Chat Latéral :</span> Posez des questions sur votre histoire, demandez des résumés ou des idées de rebondissements. L'IA connaît le contexte de vos chapitres précédents et de vos fiches personnages.
</p>
<div className="mt-6 border-t border-slate-100 pt-4">
<h4 className="font-bold text-slate-700 mb-3 flex items-center gap-2">
<Keyboard size={16} /> Raccourcis Clavier (Éditeur)
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg border border-slate-100">
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Gras</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>B</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Italique</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>I</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Souligner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>U</Kbd></span>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Tout sélectionner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>A</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Annuler</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Z</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Rétablir</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Shift</Kbd> + <Kbd>Z</Kbd></span>
</div>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-800 rounded-full">
<X size={24} />
</button>
</div>
{/* Content */}
<div className="overflow-y-auto p-8">
{/* Context Specific Content */}
{renderContent()}
{/* General Footer Section (Always visible) */}
<div className="border-t border-slate-100 pt-6 mt-6">
<h4 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">{t('help.general_shortcuts')}</h4>
<div className="grid grid-cols-2 gap-4 text-xs text-slate-600">
<div className="flex justify-between">
<span>{t('help.auto_save')}</span>
<span className="font-mono text-slate-400">{t('help.permanent')}</span>
</div>
<div className="flex justify-between">
<span>{t('help.side_menu')}</span>
<span>{t('help.click_burger')}</span>
</div>
</div>
</div>
</div>
</section>
);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white rounded-xl shadow-2xl w-[800px] max-h-[90vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-slate-900 text-white p-6 flex justify-between items-center shrink-0">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<BookOpen size={24} className="text-blue-400" /> Aide : {
viewMode === 'workflow' ? 'Workflow & Dialogues' :
viewMode === 'world_building' ? 'Bible du Monde' :
viewMode === 'settings' ? 'Paramètres' :
viewMode === 'ideas' ? 'Boîte à Idées' :
'Éditeur & IA'
}
</h2>
<p className="text-slate-400 text-sm mt-1">Astuces pour l'écran actuel.</p>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-800 rounded-full">
<X size={24} />
</button>
</div>
{/* Content */}
<div className="overflow-y-auto p-8">
{/* Context Specific Content */}
{renderContent()}
{/* General Footer Section (Always visible) */}
<div className="border-t border-slate-100 pt-6 mt-6">
<h4 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">Raccourcis Généraux</h4>
<div className="grid grid-cols-2 gap-4 text-xs text-slate-600">
<div className="flex justify-between">
<span>Sauvegarde Automatique</span>
<span className="font-mono text-slate-400">Permanente</span>
</div>
<div className="flex justify-between">
<span>Menu Latéral</span>
<span>Clic sur le burger</span>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-slate-200 bg-slate-50 flex justify-end">
<button
onClick={onClose}
className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium"
>
{t('help.close')}
</button>
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-slate-200 bg-slate-50 flex justify-end">
<button
onClick={onClose}
className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium"
>
Fermer
</button>
</div>
</div>
</div>
);
);
};
export default HelpModal;

View File

@@ -3,28 +3,31 @@
import React, { useState } from 'react';
import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types';
import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations';
interface IdeaBoardProps {
ideas: Idea[];
onUpdate: (ideas: Idea[]) => void;
}
const CATEGORIES: Record<IdeaCategory, { label: string, color: string, icon: any }> = {
plot: { label: 'Intrigue', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Lightbulb },
character: { label: 'Personnage', color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Search },
research: { label: 'Recherche', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Search },
todo: { label: 'À faire', color: 'bg-slate-100 text-slate-800 border-slate-200', icon: CheckCircle },
const CATEGORIES: Record<IdeaCategory, { labelKey: string, color: string, icon: any }> = {
plot: { labelKey: 'ideaboard.cat_plot', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Lightbulb },
character: { labelKey: 'ideaboard.cat_char', color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Search },
research: { labelKey: 'ideaboard.cat_research', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Search },
todo: { labelKey: 'ideaboard.cat_todo', color: 'bg-slate-100 text-slate-800 border-slate-200', icon: CheckCircle },
};
const STATUS_LABELS: Record<IdeaStatus, string> = {
todo: 'Idées / À faire',
progress: 'En cours',
done: 'Terminé / Validé'
todo: 'ideaboard.stat_todo',
progress: 'ideaboard.stat_prog',
done: 'ideaboard.stat_done'
};
const MAX_DESCRIPTION_LENGTH = 500;
const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
const { t } = useLanguage();
const [newIdeaTitle, setNewIdeaTitle] = useState('');
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
@@ -54,7 +57,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
};
const handleDelete = (id: string) => {
if (confirm("Supprimer cette carte ?")) {
if (confirm(t('ideaboard.delete') + " ?")) {
onUpdate(ideas.filter(i => i.id !== id));
if (editingItem?.id === id) setEditingItem(null);
}
@@ -128,12 +131,12 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, status)}
onDoubleClick={() => openQuickAdd(status)}
title="Double-cliquez dans le vide pour ajouter une carte ici"
title={t('ideaboard.empty_desc')}
>
{/* Column Header */}
<div className={`p-4 border-b border-theme-border flex justify-between items-center transition-colors duration-300 ${status === 'todo' ? 'bg-theme-bg' :
status === 'progress' ? 'bg-indigo-500/10' :
'bg-green-500/10'
status === 'progress' ? 'bg-indigo-500/10' :
'bg-green-500/10'
}`}>
<div className="flex items-center gap-2 font-bold text-theme-text">
<Icon size={18} />
@@ -172,7 +175,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
>
<div className="flex justify-between items-start mb-2">
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full flex items-center gap-1 ${CATEGORIES[idea.category].color}`}>
{CATEGORIES[idea.category].label}
{t(CATEGORIES[idea.category].labelKey as TranslationKey)}
</span>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@@ -190,7 +193,6 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
</div>
</div>
{/* CARD CONTENT */}
<div className="mb-2">
<h4 className="font-bold text-theme-text text-sm mb-1 leading-tight">{idea.title}</h4>
{idea.description && (
@@ -211,8 +213,8 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
})}
{columnIdeas.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-slate-300 text-sm italic border-2 border-dashed border-indigo-200 rounded-lg m-1">
<span className="mb-2">Vide</span>
<span className="text-xs opacity-70">Double-cliquez pour ajouter</span>
<span className="mb-2">{t('ideaboard.empty')}</span>
<span className="text-xs opacity-70">{t('ideaboard.empty_desc')}</span>
</div>
)}
</div>
@@ -227,9 +229,9 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-theme-panel p-4 rounded-xl border border-theme-border shadow-sm shrink-0 transition-colors duration-300">
<div>
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2">
<Lightbulb className="text-yellow-500" /> Boîte à Idées
<Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
</h2>
<p className="text-theme-muted text-sm">Organisez vos tâches, idées de scènes et recherches.</p>
<p className="text-theme-muted text-sm">{t('ideaboard.desc')}</p>
</div>
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-2xl flex gap-2">
@@ -239,14 +241,14 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
className="bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none transition-colors duration-300"
>
{Object.entries(CATEGORIES).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option>
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
))}
</select>
<input
type="text"
value={newIdeaTitle}
onChange={(e) => setNewIdeaTitle(e.target.value)}
placeholder="Titre de la nouvelle idée..."
placeholder={t('ideaboard.add_idea')}
className="flex-1 bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none font-medium transition-colors duration-300"
/>
<button
@@ -261,9 +263,9 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
{/* Kanban Board */}
<div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-6 min-h-0">
<Column title="Idées / À faire" status="todo" icon={Circle} />
<Column title="En cours" status="progress" icon={Clock} />
<Column title="Terminé" status="done" icon={CheckCircle} />
<Column title={t('ideaboard.stat_todo')} status="todo" icon={Circle} />
<Column title={t('ideaboard.stat_prog')} status="progress" icon={Clock} />
<Column title={t('ideaboard.stat_done')} status="done" icon={CheckCircle} />
</div>
{/* EDIT / QUICK ADD MODAL */}
@@ -273,7 +275,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="bg-theme-bg border-b border-theme-border p-4 flex justify-between items-center">
<h3 className="font-bold text-theme-text flex items-center gap-2">
{editingItem.id ? <Edit3 size={18} /> : <Plus size={18} />}
{editingItem.id ? 'Éditer la carte' : 'Ajouter une carte'}
{editingItem.id ? t('ideaboard.edit_card') : t('ideaboard.add_card')}
</h3>
<button onClick={() => setEditingItem(null)} className="text-theme-muted hover:text-theme-text">
<X size={20} />
@@ -282,19 +284,19 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="p-6 space-y-4 overflow-y-auto">
<div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Titre</label>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.title_label')}</label>
<input
type="text"
value={editingItem.title}
onChange={(e) => setEditingItem({ ...editingItem, title: e.target.value })}
className="w-full p-3 bg-theme-bg border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-bold text-theme-text transition-colors duration-300"
placeholder="Titre de la tâche ou de l'idée..."
placeholder={t('ideaboard.add_idea')}
autoFocus
/>
</div>
<div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Description</label>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.desc_label')}</label>
<textarea
value={editingItem.description}
onChange={(e) => setEditingItem({ ...editingItem, description: e.target.value })}
@@ -303,7 +305,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
placeholder="Détails, notes, liens..."
/>
<div className={`text-right text-xs mt-1 transition-colors ${(editingItem.description?.length || 0) >= MAX_DESCRIPTION_LENGTH ? 'text-red-500 font-bold' :
(editingItem.description?.length || 0) > MAX_DESCRIPTION_LENGTH * 0.9 ? 'text-orange-500' : 'text-slate-400'
(editingItem.description?.length || 0) > MAX_DESCRIPTION_LENGTH * 0.9 ? 'text-orange-500' : 'text-slate-400'
}`}>
{editingItem.description?.length || 0} / {MAX_DESCRIPTION_LENGTH} caractères
</div>
@@ -311,26 +313,26 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Catégorie</label>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.cat_label')}</label>
<select
value={editingItem.category}
onChange={(e) => setEditingItem({ ...editingItem, category: e.target.value as IdeaCategory })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded-lg text-theme-text text-sm outline-none focus:border-blue-500 transition-colors duration-300"
>
{Object.entries(CATEGORIES).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option>
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Statut</label>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.stat_label')}</label>
<select
value={editingItem.status}
onChange={(e) => setEditingItem({ ...editingItem, status: e.target.value as IdeaStatus })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded-lg text-theme-text text-sm outline-none focus:border-blue-500 transition-colors duration-300"
>
{Object.entries(STATUS_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val}</option>
<option key={key} value={key}>{t(val as TranslationKey)}</option>
))}
</select>
</div>
@@ -343,21 +345,21 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
onClick={() => handleDelete(editingItem.id!)}
className="mr-auto text-red-500 hover:text-red-700 text-sm font-medium px-3 py-2"
>
Supprimer
{t('ideaboard.delete')}
</button>
)}
<button
onClick={() => setEditingItem(null)}
className="px-4 py-2 text-theme-text hover:bg-theme-panel border border-transparent rounded-lg text-sm font-medium transition-colors duration-300"
>
Annuler
{t('ideaboard.cancel')}
</button>
<button
onClick={handleSaveEdit}
disabled={!editingItem.title?.trim()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium shadow-sm disabled:opacity-50 flex items-center gap-2"
>
<Save size={16} /> Enregistrer
<Save size={16} /> {t('ideaboard.save')}
</button>
</div>
</div>

View File

@@ -1,95 +1,106 @@
'use client';
import React from 'react';
import { Book, Sparkles, Feather, Globe, ShieldCheck, Zap, ArrowRight, Star } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface LandingPageProps {
onLogin: () => void;
onPricing: () => void;
onFeatures: () => void;
onLogin: () => void;
onPricing: () => void;
onFeatures: () => void;
}
const LandingPage: React.FC<LandingPageProps> = ({ onLogin, onPricing, onFeatures }) => {
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
{/* Navbar */}
<nav className="fixed top-0 w-full bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">PlumeIA</span>
</div>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
<button onClick={onFeatures} className="hover:text-blue-600 transition-colors">Fonctionnalités</button>
<button onClick={onPricing} className="hover:text-blue-600 transition-colors">Tarifs</button>
<a href="#" className="hover:text-blue-600 transition-colors">Blog</a>
</div>
<div className="flex items-center gap-4">
<button onClick={onLogin} className="text-sm font-bold text-slate-700 hover:text-blue-600 px-4 py-2">Connexion</button>
<button onClick={onLogin} className="bg-slate-900 text-white px-5 py-2.5 rounded-full text-sm font-bold hover:bg-blue-600 transition-all shadow-lg hover:shadow-blue-200">Essai Gratuit</button>
</div>
</nav>
const { t } = useLanguage();
{/* Hero Section */}
<header className="pt-32 pb-20 px-8 max-w-7xl mx-auto text-center">
<div className="inline-flex items-center gap-2 bg-white border border-indigo-100 px-4 py-2 rounded-full text-xs font-bold text-blue-600 mb-8 shadow-sm">
<Sparkles size={14} className="animate-pulse" /> NOUVEAUTÉ : GÉNÉRATION DE BIBLE DU MONDE PAR IA
</div>
<h1 className="text-5xl md:text-7xl font-black text-slate-900 leading-[1.1] mb-6">
L'écriture d'un roman, <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-500">augmentée par l'IA.</span>
</h1>
<p className="text-xl text-slate-600 max-w-2xl mx-auto mb-10 leading-relaxed">
PlumeIA est le premier éditeur intelligent qui comprend votre univers, vos personnages et votre style pour vous aider à franchir la page blanche.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button onClick={onLogin} className="w-full sm:w-auto bg-blue-600 text-white px-8 py-4 rounded-full text-lg font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center gap-2 justify-center">
Commencer mon livre <ArrowRight size={20} />
</button>
<button onClick={onFeatures} className="w-full sm:w-auto bg-white text-slate-900 border border-slate-200 px-8 py-4 rounded-full text-lg font-bold hover:bg-slate-50 transition-all">
Voir la démo
</button>
</div>
<div className="mt-20 relative">
<div className="absolute -inset-4 bg-gradient-to-r from-blue-500/20 to-indigo-500/20 blur-3xl -z-10 rounded-full" />
<div className="bg-white rounded-2xl shadow-2xl border border-indigo-100 p-2 overflow-hidden max-w-5xl mx-auto">
<img
src="https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&q=80&w=2000"
alt="Editor Preview"
className="rounded-xl object-cover h-[500px] w-full"
/>
</div>
</div>
</header>
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
{/* Navbar */}
<nav className="fixed top-0 w-full bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</div>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
<button onClick={onFeatures} className="hover:text-blue-600 transition-colors">{t('landing.nav_features')}</button>
<button onClick={onPricing} className="hover:text-blue-600 transition-colors">{t('landing.nav_pricing')}</button>
<a href="#" className="hover:text-blue-600 transition-colors">{t('landing.nav_blog')}</a>
</div>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<button onClick={onLogin} className="text-sm font-bold text-slate-700 hover:text-blue-600 px-4 py-2">{t('landing.login')}</button>
<button onClick={onLogin} className="bg-slate-900 text-white px-5 py-2.5 rounded-full text-sm font-bold hover:bg-blue-600 transition-all shadow-lg hover:shadow-blue-200">{t('landing.free_trial')}</button>
</div>
</nav>
{/* Social Proof */}
<section className="bg-white py-24 px-8 border-y border-indigo-100">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-slate-400 text-sm font-bold uppercase tracking-widest mb-12">Utilisé par les auteurs de demain</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-12 items-center grayscale opacity-60">
<span className="text-3xl font-serif font-black italic">FantasyMag</span>
<span className="text-2xl font-sans font-bold">Writer's Hub</span>
<span className="text-3xl font-serif">L'Éditeur</span>
<span className="text-2xl font-sans font-black tracking-tight underline underline-offset-4 decoration-blue-500">Novelty</span>
</div>
</div>
</section>
{/* Hero Section */}
<header className="pt-32 pb-20 px-8 max-w-7xl mx-auto text-center">
<div className="inline-flex items-center gap-2 bg-white border border-indigo-100 px-4 py-2 rounded-full text-xs font-bold text-blue-600 mb-8 shadow-sm">
<Sparkles size={14} className="animate-pulse" /> {t('landing.new_feature')}
</div>
<h1 className="text-5xl md:text-7xl font-black text-slate-900 leading-[1.1] mb-6">
<span dangerouslySetInnerHTML={{ __html: t('landing.hero_title') }}></span>
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-500">{t('landing.hero_subtitle')}</span>
</h1>
<p className="text-xl text-slate-600 max-w-2xl mx-auto mb-10 leading-relaxed">
{t('landing.hero_description')}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button onClick={onLogin} className="w-full sm:w-auto bg-blue-600 text-white px-8 py-4 rounded-full text-lg font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center gap-2 justify-center">
{t('landing.start_book')} <ArrowRight size={20} />
</button>
<button onClick={onFeatures} className="w-full sm:w-auto bg-white text-slate-900 border border-slate-200 px-8 py-4 rounded-full text-lg font-bold hover:bg-slate-50 transition-all">
{t('landing.see_demo')}
</button>
</div>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-8 text-center">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-center gap-2 text-white mb-6">
<Book className="text-blue-500" size={24} />
<span className="text-xl font-bold">PlumeIA</span>
</div>
<p className="text-sm">© 2024 PlumeIA. Tous droits réservés.</p>
<div className="mt-20 relative">
<div className="absolute -inset-4 bg-gradient-to-r from-blue-500/20 to-indigo-500/20 blur-3xl -z-10 rounded-full" />
<div className="bg-white rounded-2xl shadow-2xl border border-indigo-100 p-2 overflow-hidden max-w-5xl mx-auto">
<img
src="https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&q=80&w=2000"
alt="Editor Preview"
className="rounded-xl object-cover h-[500px] w-full"
/>
</div>
</div>
</header>
{/* Social Proof */}
<section className="bg-white py-24 px-8 border-y border-indigo-100">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-slate-400 text-sm font-bold uppercase tracking-widest mb-12">{t('landing.used_by')}</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-12 items-center grayscale opacity-60">
<span className="text-3xl font-serif font-black italic">FantasyMag</span>
<span className="text-2xl font-sans font-bold">Writer's Hub</span>
<span className="text-3xl font-serif">L'Éditeur</span>
<span className="text-2xl font-sans font-black tracking-tight underline underline-offset-4 decoration-blue-500">Novelty</span>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-8 text-center">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-center gap-2 text-white mb-6">
<Book className="text-blue-500" size={24} />
<span className="text-xl font-bold">Pluume</span>
</div>
<div className="flex flex-wrap items-center justify-center gap-6 mb-8 text-sm">
<Link href="/cgu" className="hover:text-blue-400 transition-colors">{t('footer.cgu')}</Link>
<Link href="/cgv" className="hover:text-blue-400 transition-colors">{t('footer.cgv')}</Link>
<Link href="/sitemap" className="hover:text-blue-400 transition-colors">{t('footer.sitemap')}</Link>
</div>
<p className="text-sm">{t('landing.copyright')}</p>
</div>
</footer>
</div>
</footer>
</div>
);
);
};
export default LandingPage;

View File

@@ -0,0 +1,67 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { SupportedLanguage } from '@/lib/i18n/translations';
import { Globe, ChevronDown } from 'lucide-react';
const languages: { code: SupportedLanguage; label: string; flag: string }[] = [
{ code: 'fr', label: 'Français', flag: 'https://flagcdn.com/fr.svg' },
{ code: 'en', label: 'English', flag: 'https://flagcdn.com/gb.svg' },
{ code: 'es', label: 'Español', flag: 'https://flagcdn.com/es.svg' },
{ code: 'de', label: 'Deutsch', flag: 'https://flagcdn.com/de.svg' },
];
export const LanguageSwitcher: React.FC = () => {
const { language, setLanguage } = useLanguage();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const currentLang = languages.find(l => l.code === language) || languages[0];
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 transition-colors"
title="Changer de langue"
>
<img src={currentLang.flag} alt={currentLang.label} className="w-5 h-3.5 object-cover rounded-[2px] shadow-sm" />
<span className="hidden sm:inline font-bold">{currentLang.code.toUpperCase()}</span>
<ChevronDown size={14} className={`transition-transform text-slate-400 ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-lg z-50 overflow-hidden">
<div className="py-1">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => {
setLanguage(lang.code);
setIsOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm flex items-center gap-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors
${language === lang.code ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium' : 'text-slate-700 dark:text-slate-300'}
`}
>
<img src={lang.flag} alt={lang.label} className="w-5 h-3.5 object-cover rounded-[2px] shadow-sm" />
{lang.label}
</button>
))}
</div>
</div>
)}
</div>
);
};

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { useAuthContext } from '@/providers/AuthProvider';
import { Loader2, AlertCircle, ArrowRight } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface LoginPageProps {
onSuccess: () => void;
@@ -17,6 +18,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
// Use the global auth context
const { login } = useAuthContext();
const { t } = useLanguage();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -37,8 +39,8 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
{/* Using styles similar to AuthPage for consistency */}
<div className="w-full max-w-md bg-white rounded-2xl shadow-xl overflow-hidden p-8 animate-in fade-in zoom-in duration-300">
<div className="text-center mb-8">
<h1 className="text-3xl font-black text-slate-900 mb-2">Connexion</h1>
<p className="text-slate-500">Bienvenue ! Connectez-vous à votre compte</p>
<h1 className="text-3xl font-black text-slate-900 mb-2">{t('auth.login_title')}</h1>
<p className="text-slate-500">{t('auth.login_subtitle')}</p>
</div>
{error && (
@@ -50,12 +52,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="email">Email</label>
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="email">{t('auth.email')}</label>
<input
id="email"
type="email"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium transition-all"
placeholder="votre@email.com"
placeholder={t('auth.email_placeholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
@@ -63,12 +65,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
</div>
<div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="password">Mot de passe</label>
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="password">{t('auth.password')}</label>
<input
id="password"
type="password"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium transition-all"
placeholder="••••••••"
placeholder={t('auth.password_placeholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
@@ -80,17 +82,17 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-6"
disabled={loading}
>
{loading ? <Loader2 className="animate-spin" /> : "Se connecter"} <ArrowRight size={18} />
{loading ? <Loader2 className="animate-spin" /> : t('auth.login_submit')} <ArrowRight size={18} />
</button>
</form>
<div className="mt-8 text-center text-sm text-slate-500">
Pas encore de compte ?{" "}
{t('auth.not_registered')}{" "}
<button
onClick={onRegister}
className="font-bold text-blue-600 hover:text-blue-800 transition-colors ml-1"
>
Créer un compte
{t('auth.create_account')}
</button>
</div>
</div>

View File

@@ -3,6 +3,9 @@
import React from 'react';
import { Check, ArrowLeft } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface PlanData {
id: string;
@@ -23,23 +26,28 @@ interface PricingProps {
}
const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectPlan, isLoading }) => {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] py-20 px-8">
<div className="max-w-6xl mx-auto">
<button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 mb-12 font-bold transition-colors">
<ArrowLeft size={20} /> Retour
</button>
<div className="flex justify-between items-center mb-12">
<button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={20} /> {t('common.back')}
</button>
<LanguageSwitcher />
</div>
<div className="text-center mb-16">
<h2 className="text-4xl font-black text-slate-900 mb-4">Choisissez votre destin d'écrivain.</h2>
<p className="text-slate-500">Passez au plan supérieur pour libérer toute la puissance de l'IA.</p>
<h2 className="text-4xl font-black text-slate-900 mb-4">{t('pricing.title')}</h2>
<p className="text-slate-500">{t('pricing.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{isLoading && <p className="text-center col-span-3 py-10">Chargement des offres...</p>}
{isLoading && <p className="text-center col-span-3 py-10">{t('pricing.loading')}</p>}
{!isLoading && plans.map((p) => (
<div key={p.id} className={`bg-white rounded-3xl p-8 border transition-all ${p.isPopular ? 'border-blue-500 shadow-2xl scale-105 z-10' : 'border-indigo-100 shadow-xl'}`}>
<div className="mb-8">
<h4 className="text-xl font-bold text-slate-900 mb-2">{p.displayName}</h4>
<div className="text-4xl font-black text-slate-900 mb-2">{p.price}<span className="text-sm font-normal text-slate-400">/mois</span></div>
<div className="text-4xl font-black text-slate-900 mb-2">{p.price}<span className="text-sm font-normal text-slate-400">{t('pricing.per_month')}</span></div>
<p className="text-sm text-slate-500">{p.description}</p>
</div>
<ul className="space-y-4 mb-10">
@@ -54,12 +62,23 @@ const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectP
onClick={() => onSelectPlan(p.id)}
className={`w-full py-4 rounded-2xl font-black transition-all ${p.id === currentPlan ? 'bg-slate-100 text-slate-400 cursor-default' : p.isPopular ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-slate-900 text-white hover:bg-slate-800'}`}
>
{p.id === currentPlan ? 'Plan Actuel' : 'Sélectionner'}
{p.id === currentPlan ? t('pricing.current_plan') : t('pricing.select')}
</button>
</div>
))}
</div>
</div>
{/* Footer */}
<footer className="mt-20 text-center border-t border-indigo-100 pt-12 relative z-20">
<div className="max-w-6xl mx-auto">
<div className="flex flex-wrap items-center justify-center gap-6 mb-8 text-sm">
<Link href="/cgu" className="text-slate-500 hover:text-blue-600 font-medium transition-colors">{t('footer.cgu')}</Link>
<Link href="/cgv" className="text-slate-500 hover:text-blue-600 font-medium transition-colors">{t('footer.cgv')}</Link>
<Link href="/sitemap" className="text-slate-500 hover:text-blue-600 font-medium transition-colors">{t('footer.sitemap')}</Link>
</div>
<p className="text-sm text-slate-400">{t('landing.copyright')}</p>
</div>
</footer>
</div>
);
};

View File

@@ -4,6 +4,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { WorkflowData, PlotNode, PlotConnection, PlotNodeType, Entity, EntityType } from '@/lib/types';
import { Plus, Trash2, ArrowRight, BookOpen, MessageCircle, Zap, Palette, Save, Link2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface StoryWorkflowProps {
data: WorkflowData;
@@ -24,8 +25,8 @@ const INITIAL_COLORS = [
'#f3e8ff', // Purple
];
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void) => {
if (!text) return <span className="text-slate-400 italic">Description...</span>;
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void, t: any) => {
if (!text) return <span className="text-slate-400 italic">{t('sw.desc_ph')}</span>;
const parts: (string | React.ReactNode)[] = [text];
@@ -45,7 +46,7 @@ const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id:
key={`${entity.id}-${idx}`}
onClick={(e) => { e.stopPropagation(); onNavigate(entity.id); }}
className="text-indigo-600 hover:text-indigo-800 underline decoration-indigo-300 hover:decoration-indigo-600 cursor-pointer font-medium bg-indigo-50 px-0.5 rounded transition-all"
title={`Voir la fiche de ${entity.name}`}
title={t('sw.see_sheet') + entity.name}
>
{s}
</span>
@@ -92,12 +93,12 @@ const StoryNode = React.memo(({
onToggleColorPicker, onSaveColor, onNavigateToEntity,
onInputFocus, onInputCheckAutocomplete, onKeyDownInInput
}: StoryNodeProps) => {
const { t } = useLanguage();
const [showTypePicker, setShowTypePicker] = useState(false);
const richDescription = useMemo(() => {
return renderTextWithLinks(node.description, entities, onNavigateToEntity);
}, [node.description, entities, onNavigateToEntity]);
return renderTextWithLinks(node.description, entities, onNavigateToEntity, t);
}, [node.description, entities, onNavigateToEntity, t]);
return (
<div
@@ -170,7 +171,7 @@ const StoryNode = React.memo(({
onClick={() => onSaveColor(node.color || '#ffffff')}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 hover:underline flex-1 text-right"
>
+ SAUVER
{t('sw.save_color')}
</button>
</div>
</div>
@@ -181,7 +182,7 @@ const StoryNode = React.memo(({
{isEditing ? (
<textarea
className={`w-full h-full bg-white/70 resize-none outline-none text-xs leading-relaxed p-2 rounded border border-indigo-100 shadow-inner ${node.type === 'dialogue' ? 'font-mono text-slate-700' : 'text-slate-600'}`}
placeholder={node.type === 'dialogue' ? "Héros: Salut !\nGuide: ..." : "Résumé de l'intrigue..."}
placeholder={node.type === 'dialogue' ? t('sw.dialogue_ph') : t('sw.plot_ph')}
value={node.description}
onChange={(e) => onInputCheckAutocomplete(e, node.id, 'description')}
onKeyDown={(e) => onKeyDownInInput(e, node.id)}
@@ -204,21 +205,21 @@ const StoryNode = React.memo(({
<button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'story' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-slate-100 ${node.type === 'story' ? 'bg-indigo-50 ring-1 ring-indigo-200' : ''}`}
title="Narration"
title={t('sw.type_story')}
>
<BookOpen size={14} className="text-slate-500" />
</button>
<button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'action' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-amber-50 ${node.type === 'action' ? 'bg-amber-50 ring-1 ring-amber-200' : ''}`}
title="Action"
title={t('sw.type_action')}
>
<Zap size={14} className="text-amber-500" />
</button>
<button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'dialogue' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-blue-50 ${node.type === 'dialogue' ? 'bg-blue-50 ring-1 ring-blue-200' : ''}`}
title="Dialogue"
title={t('sw.type_dialogue')}
>
<MessageCircle size={14} className="text-blue-500" />
</button>
@@ -267,6 +268,7 @@ interface SuggestionState {
}
const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities, onNavigateToEntity }) => {
const { t } = useLanguage();
const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null);
@@ -569,7 +571,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`,
x,
y,
title: 'Nouvel événement',
title: t('sw.new_event'),
description: '',
color: INITIAL_COLORS[0],
type: 'story'
@@ -598,7 +600,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`,
x: scrollLeft + clientWidth / 2 - CARD_WIDTH / 2,
y: scrollTop + clientHeight / 2 - CARD_HEIGHT / 2,
title: 'Nouveau point d\'intrigue',
title: t('sw.new_plot_point'),
description: '',
color: INITIAL_COLORS[0],
type: 'story'
@@ -613,15 +615,15 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
<div className="h-12 bg-theme-panel border-b border-theme-border flex items-center justify-between px-4 z-10 shadow-sm shrink-0 transition-colors duration-300">
<div className="flex items-center gap-2">
<button onClick={handleAddNodeCenter} className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-xs font-bold transition-all shadow-md shadow-indigo-100">
<Plus size={14} /> AJOUTER NŒUD
<Plus size={14} /> {t('sw.add_node')}
</button>
<div className="w-px h-6 bg-theme-border mx-2" />
<div className="text-[10px] uppercase font-bold text-theme-muted tracking-wider">
{selectedNodeIds.size > 0 ? `${selectedNodeIds.size} SÉLECTIONNÉ(S)` : 'Double-cliquez sur le canvas pour créer'}
{selectedNodeIds.size > 0 ? `${selectedNodeIds.size} ${t('sw.selected')}` : t('sw.double_click_create')}
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={handleDeleteSelected} disabled={selectedNodeIds.size === 0} className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg disabled:opacity-30 transition-colors" title="Supprimer">
<button onClick={handleDeleteSelected} disabled={selectedNodeIds.size === 0} className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg disabled:opacity-30 transition-colors" title={t('sw.delete')}>
<Trash2 size={16} />
</button>
</div>
@@ -698,7 +700,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
{activeSuggestion && (
<div className="fixed z-50 bg-white rounded-xl shadow-2xl border border-indigo-100 w-64 max-h-48 overflow-y-auto" style={{ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' }}>
<div className="px-3 py-2 bg-indigo-600 text-white text-[10px] font-black uppercase tracking-widest">
Insérer {activeSuggestion.trigger === '@' ? 'Personnage' : activeSuggestion.trigger === '#' ? 'Lieu' : 'Objet'}
{activeSuggestion.trigger === '@' ? t('sw.insert_char') : activeSuggestion.trigger === '#' ? t('sw.insert_loc') : t('sw.insert_obj')}
</div>
<div className="divide-y divide-slate-50">
{activeSuggestion.filteredEntities.length > 0 ? (
@@ -712,7 +714,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
</button>
))
) : (
<div className="p-4 text-xs text-slate-400 italic text-center">Aucun résultat</div>
<div className="p-4 text-xs text-slate-400 italic text-center">{t('sw.no_result')}</div>
)}
</div>
</div>

View File

@@ -4,6 +4,8 @@
import React, { useState } from 'react';
import { UserProfile, UserPreferences } from '@/lib/types';
import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface UserProfileSettingsProps {
user: UserProfile;
@@ -20,6 +22,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
hasOnBack: !!onBack
});
const { t } = useLanguage();
const [activeTab, setActiveTab] = useState<'profile' | 'preferences' | 'account'>('profile');
const [formData, setFormData] = useState({
name: user.name,
@@ -36,7 +39,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
alert('Veuillez sélectionner une image valide.');
alert(t('profile.invalid_image') || 'Veuillez sélectionner une image valide.');
return;
}
@@ -83,7 +86,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
dailyWordGoal: formData.dailyWordGoal
}
});
alert("Profil mis à jour !");
alert(t('profile.saved_success') || "Profil mis à jour !");
};
const isDark = formData.theme === 'dark';
@@ -102,10 +105,13 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-10">
<div>
<h1 className={`text-3xl font-black ${themeTextHeading}`}>Mon Compte</h1>
<p className={themeTextMuted}>Gérez vos informations personnelles et préférences d'écriture.</p>
<h1 className={`text-3xl font-black ${themeTextHeading}`}>{t('profile.title')}</h1>
<p className={themeTextMuted}>{t('profile.subtitle')}</p>
</div>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<button onClick={onBack} className={`${themeInnerClass} px-4 py-2 rounded-lg text-sm font-bold opacity-80 hover:opacity-100 transition-opacity`}>{t('profile.close')}</button>
</div>
<button onClick={onBack} className={`${themeInnerClass} px-4 py-2 rounded-lg text-sm font-bold opacity-80 hover:opacity-100 transition-opacity`}>Fermer</button>
</div>
<div className="flex flex-col md:flex-row gap-8">
@@ -115,19 +121,19 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
onClick={() => setActiveTab('profile')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'profile' ? themeTabActive : themeTabInactive}`}
>
<User size={18} /> Profil Public
<User size={18} /> {t('profile.tab_public')}
</button>
<button
onClick={() => setActiveTab('preferences')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'preferences' ? themeTabActive : themeTabInactive}`}
>
<Layout size={18} /> Interface & Écriture
<Layout size={18} /> {t('profile.tab_interface')}
</button>
<button
onClick={() => setActiveTab('account')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'account' ? themeTabActive : themeTabInactive}`}
>
<Shield size={18} /> Sécurité & Plan
<Shield size={18} /> {t('profile.tab_security')}
</button>
</div>
@@ -145,16 +151,16 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
className="hidden"
/>
<img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isDark ? 'border-slate-800' : isSepia ? 'border-[#f4ecd8]' : 'border-slate-50'}`} alt="Avatar" />
<div className="absolute inset-0 bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity" title="Changer d'avatar">
<div className="absolute inset-0 bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity" title={t('profile.change_avatar')}>
<Camera size={20} />
</div>
</div>
<div>
<h3 className={`font-bold text-lg ${themeTextHeading}`}>{user.name}</h3>
<p className={`text-sm ${themeTextMuted}`}>Membre depuis {new Date(user.subscription.startDate).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}</p>
<p className={`text-sm ${themeTextMuted}`}>{t('dashboard.member_since')} {new Date(user.subscription.startDate).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}</p>
<div className="mt-2 flex gap-4">
<div className="flex items-center gap-1.5 text-xs font-bold text-orange-500">
<Flame size={14} fill="currentColor" /> {user.stats.writingStreak} jours de streak
<Flame size={14} fill="currentColor" /> {user.stats.writingStreak} {t('dashboard.days')}
</div>
</div>
</div>
@@ -162,7 +168,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="grid grid-cols-1 gap-6">
<div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Nom affiché</label>
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>{t('profile.display_name')}</label>
<input
type="text"
value={formData.name}
@@ -172,12 +178,12 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
</div>
<div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Bio / Citation inspirante</label>
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>{t('profile.bio')}</label>
<textarea
value={formData.bio}
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
className={`w-full p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 h-24 resize-none ${themeInputBg}`}
placeholder="Partagez quelques mots sur votre style..."
placeholder={t('profile.bio_placeholder')}
/>
</div>
</div>
@@ -189,7 +195,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="grid grid-cols-1 gap-8">
<div className="space-y-3">
<label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}>
<Target size={14} /> Objectif quotidien de mots
<Target size={14} /> {t('profile.daily_goal')}
</label>
<div className="flex items-center gap-4">
<input
@@ -204,7 +210,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="space-y-3">
<label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}>
Thème de l'éditeur
{t('profile.editor_theme')}
</label>
<div className="grid grid-cols-3 gap-3">
{['light', 'sepia', 'dark'].map((t) => (
@@ -227,14 +233,14 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div className="p-4 bg-blue-50 border border-blue-100 rounded-xl flex justify-between items-center">
<div>
<h4 className="font-bold text-blue-900">Plan {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4>
<p className="text-xs text-blue-700">Abonnement actif</p>
<h4 className="font-bold text-blue-900">{t('profile.plan_title')} {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4>
<p className="text-xs text-blue-700">{t('profile.active_sub')}</p>
</div>
<button className="bg-blue-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-blue-700 shadow-md shadow-blue-200">Gérer</button>
<button className="bg-blue-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-blue-700 shadow-md shadow-blue-200">{t('profile.manage')}</button>
</div>
<div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Email du compte</label>
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>{t('profile.account_email')}</label>
<input
type="email"
value={formData.email}
@@ -244,7 +250,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
</div>
<div className="pt-4">
<button className="text-red-500 text-sm font-bold hover:underline">Supprimer mon compte définitivement</button>
<button className="text-red-500 text-sm font-bold hover:underline">{t('profile.delete_account')}</button>
</div>
</div>
)}
@@ -254,7 +260,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
onClick={handleSave}
className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:shadow-blue-200 ${isDark ? 'bg-white text-slate-900 hover:bg-blue-500 hover:text-white' : isSepia ? 'bg-[#5c4731] text-white hover:bg-blue-600' : 'bg-slate-900 text-white hover:bg-blue-600'}`}
>
<Save size={18} /> Sauvegarder les modifications
<Save size={18} /> {t('profile.save_changes')}
</button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
'use client';
import React, { useState, useMemo, useEffect } from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '@/lib/types';
import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft } from 'lucide-react';
import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '@/lib/constants';
@@ -32,6 +33,7 @@ const DEFAULT_CHAR_ATTRIBUTES: CharacterAttributes = {
};
const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdate, onDelete, templates, onUpdateTemplates, initialSelectedId }) => {
const { t } = useLanguage();
const [editingId, setEditingId] = useState<string | null>(null);
const [tempEntity, setTempEntity] = useState<Entity | null>(null);
const [mode, setMode] = useState<'entities' | 'templates'>('entities');
@@ -102,7 +104,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
};
const handleDelete = (id: string) => {
if (confirm('Supprimer cet élément ?')) {
if (confirm(t('wb.delete_confirm'))) {
onDelete(id);
if (editingId === id) {
setEditingId(null);
@@ -149,7 +151,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
const addCustomField = (type: EntityType) => {
const newField: CustomFieldDefinition = {
id: `field-${Date.now()}`,
label: 'Nouveau Champ',
label: t('wb.new_field'),
type: 'text',
placeholder: ''
};
@@ -209,31 +211,31 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 1: ROLE & ARCHETYPE */}
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<User size={16} /> Identité Narrative
<User size={16} /> {t('wb.id_narrative')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-xs font-semibold text-theme-muted mb-2">Archétype</label>
<label className="block text-xs font-semibold text-theme-muted mb-2">{t('wb.archetype')}</label>
<input
type="text"
list="archetype-suggestions"
value={attrs.archetype}
onChange={(e) => updateAttribute('archetype', e.target.value)}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-blue-500"
placeholder="Ex: Le Héros, Le Sage..."
placeholder={t('wb.archetype_ph')}
/>
<datalist id="archetype-suggestions">
{allArchetypes.map(a => <option key={a} value={a} />)}
</datalist>
</div>
<div>
<label className="block text-xs font-semibold text-theme-muted mb-2">Rôle dans l'histoire</label>
<label className="block text-xs font-semibold text-theme-muted mb-2">{t('wb.role')}</label>
<div className="flex gap-2 flex-wrap">
{[
{ val: 'protagonist', label: 'Protagoniste' },
{ val: 'antagonist', label: 'Antagoniste' },
{ val: 'support', label: 'Secondaire' },
{ val: 'extra', label: 'Figurant' }
{ val: 'protagonist', label: t('wb.role_protagonist') },
{ val: 'antagonist', label: t('wb.role_antagonist') },
{ val: 'support', label: t('wb.role_support') },
{ val: 'extra', label: t('wb.role_extra') }
].map(opt => (
<label key={opt.val} className={`cursor-pointer px-3 py-1.5 rounded text-xs border transition-colors ${attrs.role === opt.val ? 'bg-indigo-100 border-indigo-300 text-indigo-700 font-bold' : 'bg-theme-bg border-theme-border text-theme-muted hover:bg-theme-border'}`}>
<input
@@ -255,13 +257,13 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 2: PHYSIQUE */}
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<Ruler size={16} /> Apparence Physique
<Ruler size={16} /> {t('wb.appearance')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-6">
<div>
<div className="flex justify-between text-xs mb-1">
<label className="font-semibold text-theme-muted">Âge (ans)</label>
<label className="font-semibold text-theme-muted">{t('wb.age')}</label>
</div>
<div className="flex items-center gap-3">
<input
@@ -281,7 +283,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div>
<div className="flex justify-between text-xs mb-1">
<label className="font-semibold text-theme-muted">Taille (cm)</label>
<label className="font-semibold text-theme-muted">{t('wb.height')}</label>
</div>
<div className="flex items-center gap-3">
<input
@@ -303,7 +305,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Cheveux</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.hair')}</label>
<select
value={attrs.hair}
onChange={(e) => updateAttribute('hair', e.target.value)}
@@ -313,7 +315,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</select>
</div>
<div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Yeux</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.eyes')}</label>
<select
value={attrs.eyes}
onChange={(e) => updateAttribute('eyes', e.target.value)}
@@ -324,12 +326,12 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
</div>
<div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Signe distinctif</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.physical_quirk')}</label>
<input
type="text"
value={attrs.physicalQuirk}
onChange={(e) => updateAttribute('physicalQuirk', e.target.value)}
placeholder="Cicatrice, tatouage..."
placeholder={t('wb.physical_quirk_ph')}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400"
/>
</div>
@@ -340,15 +342,15 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 3: PSYCHOLOGIE */}
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<Brain size={16} /> Psychologie & Comportement
<Brain size={16} /> {t('wb.psychology')}
</h3>
<div className="space-y-6">
<div className="space-y-4 px-2">
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Introverti</span>
<span>Extraverti</span>
<span>{t('wb.introvert')}</span>
<span>{t('wb.extravert')}</span>
</div>
<input
type="range" min="0" max="100"
@@ -359,8 +361,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Émotionnel</span>
<span>Rationnel</span>
<span>{t('wb.emotional')}</span>
<span>{t('wb.rational')}</span>
</div>
<input
type="range" min="0" max="100"
@@ -371,8 +373,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Chaotique</span>
<span>Loyal</span>
<span>{t('wb.chaotic')}</span>
<span>{t('wb.lawful')}</span>
</div>
<input
type="range" min="0" max="100"
@@ -384,12 +386,12 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="border-t border-theme-border pt-4">
<label className="block text-xs font-semibold text-theme-muted mb-1">Toc ou habitude comportementale</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.behavioral_quirk')}</label>
<input
type="text"
value={attrs.behavioralQuirk}
onChange={(e) => updateAttribute('behavioralQuirk', e.target.value)}
placeholder="Joue avec sa bague, bégaie quand il ment..."
placeholder={t('wb.behavioral_quirk_ph')}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400"
/>
</div>
@@ -406,7 +408,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
return (
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border mt-6">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<List size={16} /> Champs Personnalisés
<List size={16} /> {t('wb.custom_fields')}
</h3>
<div className="grid grid-cols-1 gap-4">
{currentTemplate.fields.map(field => {
@@ -429,7 +431,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
onChange={(e) => updateCustomValue(field.id, e.target.value)}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400"
>
<option value="">Sélectionner...</option>
<option value="">{t('wb.select')}</option>
{field.options?.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
@@ -442,7 +444,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
onChange={(e) => updateCustomValue(field.id, e.target.checked)}
className="w-4 h-4 text-indigo-600 rounded border-theme-border focus:ring-indigo-500"
/>
<span className="text-sm text-theme-text">Activé / Oui</span>
<span className="text-sm text-theme-text">{t('wb.active_yes')}</span>
</label>
) : (
<input
@@ -469,10 +471,10 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex justify-between items-start mb-6">
<div>
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2">
<Layout size={24} className="text-indigo-600" /> Éditeur de Modèles
<Layout size={24} className="text-indigo-600" /> {t('wb.template_editor')}
</h2>
<p className="text-theme-muted text-sm mt-1">
Configurez les champs personnalisés pour chaque type de fiche.
{t('wb.template_editor_desc')}
</p>
</div>
<button onClick={() => setMode('entities')} className="p-2 text-theme-muted hover:bg-theme-border rounded-full">
@@ -486,8 +488,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
key={type}
onClick={() => setActiveTemplateType(type)}
className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${activeTemplateType === type
? 'bg-indigo-500/10 text-indigo-700 border-b-2 border-indigo-600'
: 'text-theme-muted hover:text-theme-text hover:bg-theme-panel/50'
? 'bg-indigo-500/10 text-indigo-700 border-b-2 border-indigo-600'
: 'text-theme-muted hover:text-theme-text hover:bg-theme-panel/50'
}`}
>
{type}
@@ -500,7 +502,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div key={field.id} className="bg-theme-bg border border-theme-border rounded-lg p-4 flex gap-4 items-start group">
<div className="flex-1 grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Nom du champ</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.field_name')}</label>
<input
type="text"
value={field.label}
@@ -509,28 +511,28 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
/>
</div>
<div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Type</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.field_type')}</label>
<select
value={field.type}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { type: e.target.value as CustomFieldType })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm"
>
<option value="text">Texte court</option>
<option value="textarea">Texte long</option>
<option value="number">Nombre</option>
<option value="boolean">Case à cocher</option>
<option value="select">Liste déroulante</option>
<option value="text">{t('wb.type_text')}</option>
<option value="textarea">{t('wb.type_textarea')}</option>
<option value="number">{t('wb.type_num')}</option>
<option value="boolean">{t('wb.type_bool')}</option>
<option value="select">{t('wb.type_select')}</option>
</select>
</div>
{field.type === 'select' && (
<div className="col-span-2">
<label className="block text-xs font-semibold text-theme-muted mb-1">Options (séparées par des virgules)</label>
<label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.options_desc')}</label>
<input
type="text"
value={field.options?.join(',') || ''}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { options: e.target.value.split(',').map(s => s.trim()) })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm"
placeholder="Option A, Option B, Option C"
placeholder={t('wb.options_ph')}
/>
</div>
)}
@@ -548,7 +550,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
onClick={() => addCustomField(activeTemplateType)}
className="w-full py-3 border-2 border-dashed border-theme-border rounded-lg text-theme-muted hover:border-indigo-400 hover:text-indigo-600 hover:bg-indigo-500/10 transition-all flex items-center justify-center gap-2"
>
<Plus size={20} /> Ajouter un champ
<Plus size={20} /> {t('wb.add_field')}
</button>
</div>
</div>
@@ -560,7 +562,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex h-full gap-6 p-6 bg-theme-bg">
<div className="w-1/3 opacity-50 pointer-events-none filter blur-[1px]">
<div className="bg-theme-panel rounded-lg p-6 shadow-sm border border-theme-border">
<h3 className="font-bold text-theme-text mb-4">Aperçu Fiches</h3>
<h3 className="font-bold text-theme-text mb-4">{t('wb.preview_cards')}</h3>
<div className="space-y-2">
<div className="h-10 bg-indigo-500/10 rounded"></div>
<div className="h-10 bg-indigo-500/10 rounded"></div>
@@ -577,13 +579,13 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex h-full gap-6 p-6 bg-theme-bg">
<div className="w-1/3 flex flex-col gap-4">
<div className="flex justify-between items-center px-1">
<h2 className="text-lg font-bold text-theme-text">Explorateur</h2>
<h2 className="text-lg font-bold text-theme-text">{t('wb.explorer')}</h2>
<button
onClick={() => setMode('templates')}
className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-100 text-indigo-700 hover:bg-indigo-200 rounded text-xs font-medium transition-colors"
title="Gérer les modèles de fiches"
title={t('wb.manage_templates')}
>
<Settings size={14} /> Modèles
<Settings size={14} /> {t('wb.templates')}
</button>
</div>
@@ -603,7 +605,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="divide-y divide-slate-100">
{filterByType(type).length === 0 && (
<p className="p-4 text-sm text-theme-muted italic text-center">Aucun élément</p>
<p className="p-4 text-sm text-theme-muted italic text-center">{t('wb.no_element')}</p>
)}
{filterByType(type).map(entity => (
<div
@@ -638,7 +640,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{tempEntity.type}
</span>
<h2 className="text-2xl font-bold text-theme-text">
{tempEntity.type === EntityType.CHARACTER ? 'Fiche Personnage' : 'Édition de la fiche'}
{tempEntity.type === EntityType.CHARACTER ? t('wb.char_sheet') : t('wb.edit_sheet')}
</h2>
</div>
<div className="flex gap-2">
@@ -650,23 +652,23 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-theme-text mb-1">Nom</label>
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.name')}</label>
<input
type="text"
value={tempEntity.name}
onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none font-serif text-lg"
placeholder="Ex: Gandalf le Gris"
placeholder={t('wb.name_ph')}
/>
</div>
<div>
<label className="block text-sm font-medium text-theme-text mb-1">Description Courte (pour l'IA)</label>
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.short_desc')}</label>
<textarea
value={tempEntity.description}
onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-20"
placeholder="Un magicien puissant qui guide la communauté..."
placeholder={t('wb.short_desc_ph')}
/>
</div>
@@ -677,23 +679,23 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="mt-6 border-t border-theme-border pt-6">
<div>
<label className="block text-sm font-medium text-indigo-700 mb-1 flex items-center gap-2">
<Sparkles size={14} /> Contexte Narratif (Auto-généré)
<Sparkles size={14} /> {t('wb.story_context')}
</label>
<textarea
value={tempEntity.storyContext || ''}
onChange={e => setTempEntity({ ...tempEntity, storyContext: e.target.value })}
className="w-full p-2 border border-indigo-200 bg-indigo-500/10 rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-24 italic text-theme-muted"
placeholder="Les événements vécus par ce personnage apparaîtront ici..."
placeholder={t('wb.story_context_ph')}
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-theme-text mb-1">Notes & Biographie Complète</label>
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.notes_bio')}</label>
<textarea
value={tempEntity.details}
onChange={e => setTempEntity({ ...tempEntity, details: e.target.value })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none h-48 font-serif"
placeholder="Histoire détaillée, secrets, origines..."
placeholder={t('wb.notes_bio_ph')}
/>
</div>
</div>
@@ -704,7 +706,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-md"
>
<Save size={18} />
Enregistrer la fiche
{t('wb.save')}
</button>
</div>
</div>
@@ -712,8 +714,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
) : (
<div className="h-full flex flex-col items-center justify-center text-theme-muted">
<div className="text-6xl mb-4 opacity-20">🌍</div>
<p className="text-lg">Sélectionnez ou créez une fiche pour commencer.</p>
<p className="text-sm">Ces informations aideront l'IA à rester cohérente.</p>
<p className="text-lg">{t('wb.select_start')}</p>
<p className="text-sm">{t('wb.ai_help')}</p>
</div>
)}
</div>

View File

@@ -5,6 +5,8 @@ import React, { useState } from 'react';
import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types';
import AIPanel from '@/components/AIPanel';
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface EditorShellProps {
project: BookProject;
@@ -30,6 +32,7 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
const { project, user, viewMode, currentChapterId, children } = props;
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
const { t } = useLanguage();
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
@@ -40,23 +43,26 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
<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">
<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" /> Pluume
</h1>
<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"
placeholder={t('header.title_placeholder')}
/>
<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
<LayoutDashboard size={14} /> {t('nav.dashboard')}
</button>
<div className="mt-2 text-slate-400">
<LanguageSwitcher />
</div>
</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>
{t('nav.chapters')} <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">
@@ -70,26 +76,26 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
</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} /> Retour à la rédaction</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 className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">{t('nav.tools')}</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} /> {t('sidebar.write')}</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} /> {t('sidebar.world_building')}</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} /> {t('sidebar.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} /> {t('sidebar.ideas')}</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} /> {t('sidebar.settings')}</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>{t('sidebar.ai_actions')}</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>
<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} /> {t('sidebar.account')}</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} /> {t('sidebar.logout')}</button>
</div>
</aside>

1318
src/lib/i18n/translations.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import { translations, SupportedLanguage, TranslationKey } from '@/lib/i18n/translations';
interface LanguageContextType {
language: SupportedLanguage;
setLanguage: (lang: SupportedLanguage) => void;
t: (key: TranslationKey) => string;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [language, setLanguageState] = useState<SupportedLanguage>('fr');
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
const storedLang = localStorage.getItem('pluume_language') as SupportedLanguage;
if (storedLang && Object.keys(translations).includes(storedLang)) {
setLanguageState(storedLang);
}
}, []);
const setLanguage = (lang: SupportedLanguage) => {
setLanguageState(lang);
localStorage.setItem('pluume_language', lang);
};
const t = (key: TranslationKey): string => {
if (!isMounted) return translations.fr[key] || key; // SSR fallback
return translations[language][key] || key;
};
return (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = () => {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};