Multi lingua, hotfixes and design
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { BookOpen, Image, Settings } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -7,6 +7,7 @@ import AuthModal from './components/auth/AuthModal';
|
||||
import CoverDesigner from './components/cover-designer/CoverDesigner';
|
||||
import BookGenerator from './components/book-generator/BookGenerator';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import OnboardingPage from './components/OnboardingPage';
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
@@ -23,12 +24,12 @@ function App() {
|
||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="/" className="flex items-center gap-2 no-underline">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-accent-500 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-gray-900">CreaBook</h1>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex items-center gap-1">
|
||||
@@ -84,7 +85,7 @@ function App() {
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/covers" replace />} />
|
||||
<Route path="/" element={<OnboardingPage />} />
|
||||
<Route path="/covers" element={<CoverDesigner />} />
|
||||
<Route path="/books" element={<BookGenerator />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
|
||||
124
client/src/components/OnboardingPage.tsx
Normal file
124
client/src/components/OnboardingPage.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Globe, BookOpen, Wand2, Edit3 } from 'lucide-react';
|
||||
import { booksApi } from '../services/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [idea, setIdea] = useState('');
|
||||
const [genre, setGenre] = useState('fiction');
|
||||
|
||||
const { data: genresData } = useQuery({
|
||||
queryKey: ['genres'],
|
||||
queryFn: async () => {
|
||||
const response = await booksApi.getGenres();
|
||||
return response.data.genres;
|
||||
},
|
||||
});
|
||||
|
||||
const handleLanguageChange = (lang: string) => {
|
||||
i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
const startJourney = (useAI: boolean) => {
|
||||
navigate('/books', { state: { autoBook: { idea, genre, useAI } } });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto py-12 px-6">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-extrabold text-gray-900 mb-4">{t('onboarding.title')}</h1>
|
||||
<p className="text-lg text-gray-600">{t('onboarding.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-12">
|
||||
{/* Step 1: Language */}
|
||||
<section className="card">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2 mb-4">
|
||||
<Globe className="text-primary-500 w-6 h-6" />
|
||||
{t('onboarding.step1')}
|
||||
</h2>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => handleLanguageChange('en')}
|
||||
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
|
||||
i18n.language === 'en'
|
||||
? 'bg-primary-500 text-white shadow-md'
|
||||
: 'bg-white text-gray-700 border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleLanguageChange('es')}
|
||||
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
|
||||
i18n.language === 'es'
|
||||
? 'bg-primary-500 text-white shadow-md'
|
||||
: 'bg-white text-gray-700 border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Español
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Step 2: Book Idea */}
|
||||
<section className="card">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2 mb-4">
|
||||
<BookOpen className="text-primary-500 w-6 h-6" />
|
||||
{t('onboarding.step2')}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="label">{t('onboarding.genreLabel')}</label>
|
||||
<select
|
||||
value={genre}
|
||||
onChange={(e) => setGenre(e.target.value)}
|
||||
className="input bg-white"
|
||||
>
|
||||
{genresData?.map((g: any) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{String(t(`genres.${g.id}.name`, g.name))}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={idea}
|
||||
onChange={(e) => setIdea(e.target.value)}
|
||||
placeholder={t('onboarding.ideaPlaceholder')}
|
||||
className="input text-lg resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Step 3: Progression */}
|
||||
<section className="text-center pt-4">
|
||||
<h2 className="text-xl font-bold mb-6">{t('onboarding.step3')}</h2>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => startJourney(false)}
|
||||
className="flex flex-col items-center justify-center p-6 bg-white border-2 border-gray-200 hover:border-gray-400 rounded-xl transition-all"
|
||||
>
|
||||
<Edit3 className="w-10 h-10 text-gray-500 mb-3" />
|
||||
<span className="font-semibold text-gray-800 text-lg">{t('onboarding.btnFree')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => startJourney(true)}
|
||||
className="flex flex-col items-center justify-center p-6 bg-primary-50 border-2 border-primary-500 hover:bg-primary-100 rounded-xl transition-all transform hover:scale-[1.02]"
|
||||
>
|
||||
<Wand2 className="w-10 h-10 text-primary-600 mb-3" />
|
||||
<span className="font-semibold text-primary-800 text-lg">{t('onboarding.btnAI')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
219
client/src/components/RichTextEditor.tsx
Normal file
219
client/src/components/RichTextEditor.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import TextAlign from '@tiptap/extension-text-align';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import {
|
||||
Bold, Italic, Underline as UnderlineIcon, Strikethrough,
|
||||
List, ListOrdered, AlignLeft, AlignCenter, AlignRight,
|
||||
Undo2, Redo2, Heading1, Heading2, Heading3, Quote, Minus,
|
||||
} from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface RichTextEditorProps {
|
||||
content: string;
|
||||
onChange: (html: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
onClick,
|
||||
active,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
active?: boolean;
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
active
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2, 3] },
|
||||
}),
|
||||
Underline,
|
||||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder || 'Start writing...',
|
||||
}),
|
||||
],
|
||||
content,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML());
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
'prose prose-lg max-w-none min-h-[500px] focus:outline-none p-6 font-serif leading-relaxed text-center',
|
||||
},
|
||||
},
|
||||
onCreate: ({ editor }) => {
|
||||
// Set default text alignment to center for completely new/empty content
|
||||
if (!content || content.trim() === '' || content.trim() === '<p></p>') {
|
||||
editor.chain().focus().setTextAlign('center').run();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Sync external content changes (e.g. AI-generated content)
|
||||
useEffect(() => {
|
||||
if (editor && content !== editor.getHTML()) {
|
||||
editor.commands.setContent(content);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-0.5 px-3 py-2 bg-gray-50 border-b border-gray-200">
|
||||
{/* Undo / Redo */}
|
||||
<ToolbarButton onClick={() => editor.chain().focus().undo().run()} title="Undo">
|
||||
<Undo2 className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().redo().run()} title="Redo">
|
||||
<Redo2 className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-5 bg-gray-300 mx-1" />
|
||||
|
||||
{/* Headings */}
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
active={editor.isActive('heading', { level: 1 })}
|
||||
title="Heading 1"
|
||||
>
|
||||
<Heading1 className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
active={editor.isActive('heading', { level: 2 })}
|
||||
title="Heading 2"
|
||||
>
|
||||
<Heading2 className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
active={editor.isActive('heading', { level: 3 })}
|
||||
title="Heading 3"
|
||||
>
|
||||
<Heading3 className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-5 bg-gray-300 mx-1" />
|
||||
|
||||
{/* Inline formatting */}
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
active={editor.isActive('bold')}
|
||||
title="Bold"
|
||||
>
|
||||
<Bold className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
active={editor.isActive('italic')}
|
||||
title="Italic"
|
||||
>
|
||||
<Italic className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
active={editor.isActive('underline')}
|
||||
title="Underline"
|
||||
>
|
||||
<UnderlineIcon className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
active={editor.isActive('strike')}
|
||||
title="Strikethrough"
|
||||
>
|
||||
<Strikethrough className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-5 bg-gray-300 mx-1" />
|
||||
|
||||
{/* Lists */}
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
active={editor.isActive('bulletList')}
|
||||
title="Bullet List"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
active={editor.isActive('orderedList')}
|
||||
title="Ordered List"
|
||||
>
|
||||
<ListOrdered className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-5 bg-gray-300 mx-1" />
|
||||
|
||||
{/* Alignment */}
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
active={editor.isActive({ textAlign: 'left' })}
|
||||
title="Align Left"
|
||||
>
|
||||
<AlignLeft className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
active={editor.isActive({ textAlign: 'center' })}
|
||||
title="Align Center"
|
||||
>
|
||||
<AlignCenter className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
active={editor.isActive({ textAlign: 'right' })}
|
||||
title="Align Right"
|
||||
>
|
||||
<AlignRight className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-5 bg-gray-300 mx-1" />
|
||||
|
||||
{/* Block elements */}
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
active={editor.isActive('blockquote')}
|
||||
title="Blockquote"
|
||||
>
|
||||
<Quote className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
title="Horizontal Rule"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
|
||||
{/* Editor content */}
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { Wand2, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBookStore } from '../../stores/bookStore';
|
||||
import { booksApi } from '../../services/api';
|
||||
import RichTextEditor from '../RichTextEditor';
|
||||
|
||||
export default function BookEditor() {
|
||||
const { activeBook, updateChapterContent, setCurrentChapter } = useBookStore();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [showAIHelp, setShowAIHelp] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!activeBook || !activeBook.outline) {
|
||||
return (
|
||||
<div className="card text-center py-12">
|
||||
<p className="text-gray-500">
|
||||
Generate an outline first to start writing your book.
|
||||
{t('bookEditor.noOutline')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -51,6 +54,7 @@ export default function BookEditor() {
|
||||
previousChapter?.content
|
||||
);
|
||||
|
||||
console.log('Chapter generation response:', response.data);
|
||||
updateChapterContent(activeBook.currentChapter, response.data.content);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate chapter:', error);
|
||||
@@ -69,7 +73,7 @@ export default function BookEditor() {
|
||||
{/* Chapter Navigation */}
|
||||
<div className="space-y-4">
|
||||
<div className="card">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Chapters</h3>
|
||||
<h3 className="font-semibold text-gray-900 mb-4">{t('bookEditor.chapters')}</h3>
|
||||
<div className="space-y-2">
|
||||
{activeBook.outline.chapters.map((chapter) => (
|
||||
<button
|
||||
@@ -88,7 +92,7 @@ export default function BookEditor() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{chapter.title}</p>
|
||||
{chapter.content && (
|
||||
<span className="text-xs text-green-600">✓ Written</span>
|
||||
<span className="text-xs text-green-600">✓ {t('bookEditor.written')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +103,7 @@ export default function BookEditor() {
|
||||
|
||||
{/* Chapter Info */}
|
||||
<div className="card">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Chapter Summary</h4>
|
||||
<h4 className="font-medium text-gray-900 mb-2">{t('bookEditor.chapterSummary')}</h4>
|
||||
<p className="text-sm text-gray-600">{currentChapter?.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +126,7 @@ export default function BookEditor() {
|
||||
{currentChapter?.title}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Chapter {activeBook.currentChapter} of {activeBook.outline.chapters.length}
|
||||
{t('bookEditor.chapterOf', { current: activeBook.currentChapter, total: activeBook.outline.chapters.length })}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -140,7 +144,7 @@ export default function BookEditor() {
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
AI Assist
|
||||
{t('bookEditor.aiAssist')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerateChapter}
|
||||
@@ -148,7 +152,7 @@ export default function BookEditor() {
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Wand2 className="w-4 h-4" />
|
||||
{isGenerating ? 'Generating...' : 'Generate Chapter'}
|
||||
{isGenerating ? t('bookEditor.generating') : t('bookEditor.generateChapter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,22 +160,22 @@ export default function BookEditor() {
|
||||
{/* AI Help Panel */}
|
||||
{showAIHelp && (
|
||||
<div className="mb-6 bg-gradient-to-r from-accent-50 to-primary-50 rounded-lg p-4 border border-accent-200">
|
||||
<h4 className="font-medium text-gray-900 mb-3">AI Writing Assistant</h4>
|
||||
<h4 className="font-medium text-gray-900 mb-3">{t('bookEditor.aiWritingAssistant')}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={handleExpandText}
|
||||
className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors"
|
||||
>
|
||||
✨ Expand this section
|
||||
✨ {t('bookEditor.expandSection')}
|
||||
</button>
|
||||
<button className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors">
|
||||
📝 Improve prose
|
||||
📝 {t('bookEditor.improveProse')}
|
||||
</button>
|
||||
<button className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors">
|
||||
💡 Add description
|
||||
💡 {t('bookEditor.addDescription')}
|
||||
</button>
|
||||
<button className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors">
|
||||
🔄 Rewrite paragraph
|
||||
🔄 {t('bookEditor.rewriteParagraph')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,24 +183,13 @@ export default function BookEditor() {
|
||||
|
||||
{/* Editor */}
|
||||
<div className="min-h-[500px]">
|
||||
{currentChapter?.content ? (
|
||||
<textarea
|
||||
value={currentChapter.content}
|
||||
onChange={(e) =>
|
||||
updateChapterContent(activeBook.currentChapter, e.target.value)
|
||||
}
|
||||
className="w-full h-[600px] p-6 font-serif text-lg leading-relaxed border-0 focus:ring-0 resize-y outline-none"
|
||||
placeholder="Start writing or use AI to generate this chapter..."
|
||||
/>
|
||||
) : (
|
||||
<div className="h-[500px] flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<Wand2 className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>Click "Generate Chapter" to create content with AI</p>
|
||||
<p className="text-sm mt-1">Or start writing manually</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<RichTextEditor
|
||||
content={currentChapter?.content || ''}
|
||||
onChange={(html) =>
|
||||
updateChapterContent(activeBook.currentChapter, html)
|
||||
}
|
||||
placeholder={t('bookEditor.editorPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Word Count */}
|
||||
@@ -205,10 +198,10 @@ export default function BookEditor() {
|
||||
{currentChapter?.content
|
||||
? currentChapter.content.split(/\s+/).length
|
||||
: 0}{' '}
|
||||
words
|
||||
{t('bookEditor.words')}
|
||||
</span>
|
||||
<span>
|
||||
Last updated:{' '}
|
||||
{t('bookEditor.lastUpdated')}{' '}
|
||||
{new Date(activeBook.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { BookOpen, Wand2, Edit3, Users, FileText } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import GenreSelector from './GenreSelector';
|
||||
import BookOutlineGenerator from './BookOutlineGenerator';
|
||||
import BookEditor from './BookEditor';
|
||||
@@ -10,6 +12,31 @@ import { useQuery } from '@tanstack/react-query';
|
||||
export default function BookGenerator() {
|
||||
const [activeTab, setActiveTab] = useState<'genre' | 'outline' | 'editor' | 'characters'>('genre');
|
||||
const { activeBook, setActiveBook, setOutline } = useBookStore();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Auto-initialize from onboarding page state
|
||||
useEffect(() => {
|
||||
const state = location.state as { autoBook?: { idea: string; genre: string; useAI: boolean } } | null;
|
||||
if (state?.autoBook) {
|
||||
const { idea, genre, useAI } = state.autoBook;
|
||||
const newBook: Book = {
|
||||
id: `book-${Date.now()}`,
|
||||
title: idea.substring(0, 50) || 'Untitled',
|
||||
genre,
|
||||
idea,
|
||||
outline: null,
|
||||
characters: [],
|
||||
currentChapter: 1,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
setActiveBook(newBook);
|
||||
setActiveTab(useAI ? 'outline' : 'editor');
|
||||
// Clear the state so refreshing doesn't re-trigger
|
||||
window.history.replaceState({}, document.title);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { data: genresData } = useQuery({
|
||||
queryKey: ['genres'],
|
||||
@@ -46,9 +73,9 @@ export default function BookGenerator() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Book Generator</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{t('bookGenerator.title')}</h2>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Generate book ideas and write with AI assistance
|
||||
{t('bookGenerator.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +91,7 @@ export default function BookGenerator() {
|
||||
disabled={!activeBook && activeTab !== 'genre'}
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Genre</span>
|
||||
<span className="hidden sm:inline">{t('bookGenerator.tabGenre')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('outline')}
|
||||
@@ -76,7 +103,7 @@ export default function BookGenerator() {
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Outline</span>
|
||||
<span className="hidden sm:inline">{t('bookGenerator.tabOutline')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('editor')}
|
||||
@@ -88,7 +115,7 @@ export default function BookGenerator() {
|
||||
}`}
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Write</span>
|
||||
<span className="hidden sm:inline">{t('bookGenerator.tabWrite')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('characters')}
|
||||
@@ -100,7 +127,7 @@ export default function BookGenerator() {
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Characters</span>
|
||||
<span className="hidden sm:inline">{t('bookGenerator.tabCharacters')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +160,7 @@ export default function BookGenerator() {
|
||||
function CharacterGenerator() {
|
||||
const { activeBook, addCharacter } = useBookStore();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleGenerateCharacters = async () => {
|
||||
if (!activeBook) return;
|
||||
@@ -166,7 +194,7 @@ function CharacterGenerator() {
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Character Development
|
||||
{t('characters.title')}
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleGenerateCharacters}
|
||||
@@ -174,7 +202,7 @@ function CharacterGenerator() {
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Wand2 className="w-4 h-4" />
|
||||
{isGenerating ? 'Generating...' : 'Generate Characters'}
|
||||
{isGenerating ? t('characters.generating') : t('characters.generate')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +210,7 @@ function CharacterGenerator() {
|
||||
<div className="text-center py-8">
|
||||
<Users className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500">
|
||||
No characters yet. Click "Generate Characters" to create AI-suggested characters.
|
||||
{t('characters.empty')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -206,11 +234,11 @@ function CharacterGenerator() {
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Traits:</span>
|
||||
<span className="text-gray-500">{t('characters.traits')}:</span>
|
||||
<p className="text-gray-700">{character.traits?.join(', ') || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Motivation:</span>
|
||||
<span className="text-gray-500">{t('characters.motivation')}:</span>
|
||||
<p className="text-gray-700">{character.motivation || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Wand2, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Book, useBookStore } from '../../stores/bookStore';
|
||||
import { booksApi } from '../../services/api';
|
||||
|
||||
@@ -12,6 +13,7 @@ export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGe
|
||||
const { activeBook } = useBookStore();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const outline = activeBook?.outline;
|
||||
|
||||
@@ -22,9 +24,12 @@ export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGe
|
||||
try {
|
||||
const response = await booksApi.generateOutline(book.genre, book.idea, book.title);
|
||||
const generatedOutline = response.data.outline;
|
||||
console.log('Generated outline:', generatedOutline);
|
||||
console.log('Outline chapters:', generatedOutline?.chapters);
|
||||
onComplete(generatedOutline);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to generate outline');
|
||||
console.error('Outline generation error:', err);
|
||||
setError(err.response?.data?.error || t('bookOutline.failedToGenerate'));
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
@@ -36,8 +41,8 @@ export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGe
|
||||
<div className="card bg-gradient-to-r from-primary-50 to-accent-50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{book.title || 'Untitled Book'}</h3>
|
||||
<p className="text-sm text-gray-600 mt-1 capitalize">Genre: {book.genre}</p>
|
||||
<h3 className="font-semibold text-gray-900">{book.title || t('bookOutline.untitledBook')}</h3>
|
||||
<p className="text-sm text-gray-600 mt-1 capitalize">{t('bookOutline.genre')}: {book.genre}</p>
|
||||
<p className="text-sm text-gray-500 mt-2 line-clamp-2">{book.idea}</p>
|
||||
</div>
|
||||
{!outline && (
|
||||
@@ -49,12 +54,12 @@ export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGe
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Generating...
|
||||
{t('bookOutline.generatingOutline')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 className="w-4 h-4" />
|
||||
Generate Outline
|
||||
{t('bookOutline.generateOutline')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -72,51 +77,68 @@ export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGe
|
||||
{/* Outline Display */}
|
||||
{outline && (
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Generated Outline: {outline.title || book.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{outline.logline && (
|
||||
<div className="mb-6 pb-6 border-b border-gray-200">
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Logline</h4>
|
||||
<p className="text-gray-900 italic">{outline.logline}</p>
|
||||
{outline.error ? (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="text-red-800 font-medium mb-2">Generation Error</h4>
|
||||
<p className="text-red-700 text-sm">{outline.error}</p>
|
||||
{outline.raw && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-red-600 cursor-pointer">Raw AI Response</summary>
|
||||
<pre className="text-xs text-gray-600 mt-2 whitespace-pre-wrap bg-gray-100 p-2 rounded">
|
||||
{outline.raw}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{t('bookOutline.generatedOutline')}: {outline.title || book.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-4">Chapter Outline</h4>
|
||||
<div className="space-y-3">
|
||||
{outline.chapters?.map((chapter: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-8 h-8 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{chapter.number || index + 1}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h5 className="font-medium text-gray-900">
|
||||
{chapter.title || `Chapter ${chapter.number || index + 1}`}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{chapter.summary || 'No summary available'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{outline.logline && (
|
||||
<div className="mb-6 pb-6 border-b border-gray-200">
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">{t('bookOutline.logline')}</h4>
|
||||
<p className="text-gray-900 italic">{outline.logline}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
<p className="text-sm text-gray-500">
|
||||
Ready to start writing! Navigate to the Write tab to begin.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-4">{t('bookOutline.chapterOutline')}</h4>
|
||||
<div className="space-y-3">
|
||||
{outline.chapters?.map((chapter: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-8 h-8 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{chapter.number || index + 1}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h5 className="font-medium text-gray-900">
|
||||
{chapter.title || `${t('bookOutline.chapter')} ${chapter.number || index + 1}`}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{chapter.summary || t('bookOutline.noSummary')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('bookOutline.readyToWrite')}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -124,10 +146,9 @@ export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGe
|
||||
{!outline && !isGenerating && (
|
||||
<div className="card text-center py-12">
|
||||
<Wand2 className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">Ready to Generate</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900">{t('bookOutline.readyToGenerate')}</h3>
|
||||
<p className="text-gray-500 mt-2 max-w-md mx-auto">
|
||||
Click "Generate Outline" to create a detailed chapter outline based on your genre and book idea.
|
||||
The AI will create a structure following genre-specific patterns.
|
||||
{t('bookOutline.readyToGenerateDesc')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BookOpen, Sparkles } from 'lucide-react';
|
||||
|
||||
interface Genre {
|
||||
@@ -14,6 +15,7 @@ interface GenreSelectorProps {
|
||||
}
|
||||
|
||||
export default function GenreSelector({ genres, onSelect }: GenreSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedGenre, setSelectedGenre] = useState<string>('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [idea, setIdea] = useState('');
|
||||
@@ -32,7 +34,7 @@ export default function GenreSelector({ genres, onSelect }: GenreSelectorProps)
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<BookOpen className="w-5 h-5" />
|
||||
Select Your Genre
|
||||
{t('genreSelector.selectGenre')}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
@@ -47,9 +49,11 @@ export default function GenreSelector({ genres, onSelect }: GenreSelectorProps)
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl mb-2 block">{genre.icon}</span>
|
||||
<h4 className="font-medium text-gray-900">{genre.name}</h4>
|
||||
<h4 className="font-medium text-gray-900">
|
||||
{t(`genres.${genre.id}.name`, genre.name)}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
|
||||
{genre.description}
|
||||
{t(`genres.${genre.id}.description`, genre.description)}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
@@ -62,29 +66,29 @@ export default function GenreSelector({ genres, onSelect }: GenreSelectorProps)
|
||||
<div className="card sticky top-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
Book Details
|
||||
{t('genreSelector.bookDetails')}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="label">Book Title (optional)</label>
|
||||
<label className="label">{t('genreSelector.bookTitle')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="My Amazing Book"
|
||||
placeholder={t('genreSelector.bookTitlePlaceholder')}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">
|
||||
Core Idea <span className="text-red-500">*</span>
|
||||
{t('genreSelector.coreIdea')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={idea}
|
||||
onChange={(e) => setIdea(e.target.value)}
|
||||
placeholder="Describe your book idea... What's the story about? Who are the main characters? What conflict drives the narrative?"
|
||||
placeholder={t('genreSelector.coreIdeaPlaceholder')}
|
||||
className="input min-h-[150px] resize-y"
|
||||
required
|
||||
/>
|
||||
@@ -92,7 +96,7 @@ export default function GenreSelector({ genres, onSelect }: GenreSelectorProps)
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-xs text-amber-700">
|
||||
<strong>Tip:</strong> The more details you provide, the better the AI can generate your outline and content.
|
||||
<strong>Tip:</strong> {t('genreSelector.tip')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +105,7 @@ export default function GenreSelector({ genres, onSelect }: GenreSelectorProps)
|
||||
disabled={!selectedGenre || !idea.trim()}
|
||||
className="btn-primary w-full py-3"
|
||||
>
|
||||
Generate Outline
|
||||
{t('genreSelector.generateOutline')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Image, Wand2, Type } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CoverGallery from './CoverGallery';
|
||||
import CoverEditor from './CoverEditor';
|
||||
import { useCoverStore, Cover } from '../../stores/coverStore';
|
||||
@@ -10,6 +11,8 @@ export default function CoverDesigner() {
|
||||
const [activeTab, setActiveTab] = useState<'gallery' | 'editor' | 'ai'>('gallery');
|
||||
const { activeCover, setActiveCover } = useCoverStore();
|
||||
const queryClient = useQueryClient();
|
||||
const { t: tMain } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: coversData } = useQuery({
|
||||
queryKey: ['covers'],
|
||||
@@ -37,8 +40,8 @@ export default function CoverDesigner() {
|
||||
const handleSelectCover = (coverUrl: string) => {
|
||||
const newCover: Cover = {
|
||||
id: `cover-${Date.now()}`,
|
||||
title: 'New Book',
|
||||
author: 'Author Name',
|
||||
title: tMain('coverDesigner.newBookTitle'),
|
||||
author: tMain('coverDesigner.authorName'),
|
||||
backgroundImage: coverUrl,
|
||||
backgroundColor: '#ffffff',
|
||||
width: 1600,
|
||||
@@ -55,9 +58,9 @@ export default function CoverDesigner() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Cover Designer</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{tMain('coverDesigner.title')}</h2>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Design stunning book covers with AI or manual editing
|
||||
{tMain('coverDesigner.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +75,7 @@ export default function CoverDesigner() {
|
||||
}`}
|
||||
>
|
||||
<Image className="w-4 h-4" />
|
||||
Gallery
|
||||
{tMain('coverDesigner.tabGallery')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('editor')}
|
||||
@@ -83,7 +86,7 @@ export default function CoverDesigner() {
|
||||
}`}
|
||||
>
|
||||
<Type className="w-4 h-4" />
|
||||
Editor
|
||||
{tMain('coverDesigner.tabEditor')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('ai')}
|
||||
@@ -94,7 +97,7 @@ export default function CoverDesigner() {
|
||||
}`}
|
||||
>
|
||||
<Wand2 className="w-4 h-4" />
|
||||
AI Generate
|
||||
{tMain('coverDesigner.tabAIGenerate')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,36 +134,37 @@ function AIGenerator({
|
||||
}) {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [selectedStyle, setSelectedStyle] = useState('modern');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const styles = [
|
||||
{ id: 'modern', name: 'Modern', icon: '✨' },
|
||||
{ id: 'minimal', name: 'Minimalist', icon: '🎯' },
|
||||
{ id: 'vintage', name: 'Vintage', icon: '📜' },
|
||||
{ id: 'bold', name: 'Bold', icon: '🔥' },
|
||||
{ id: 'elegant', name: 'Elegant', icon: '💎' },
|
||||
{ id: 'playful', name: 'Playful', icon: '🎨' },
|
||||
{ id: 'modern', name: t('coverDesigner.aiGenerator.modern'), icon: '✨' },
|
||||
{ id: 'minimalist', name: t('coverDesigner.aiGenerator.minimalist'), icon: '🎯' },
|
||||
{ id: 'vintage', name: t('coverDesigner.aiGenerator.vintage'), icon: '📜' },
|
||||
{ id: 'bold', name: t('coverDesigner.aiGenerator.bold'), icon: '🔥' },
|
||||
{ id: 'elegant', name: t('coverDesigner.aiGenerator.elegant'), icon: '💎' },
|
||||
{ id: 'playful', name: t('coverDesigner.aiGenerator.playful'), icon: '🎨' },
|
||||
];
|
||||
|
||||
const genrePresets = [
|
||||
{
|
||||
genre: 'Mystery',
|
||||
prompt: 'A mysterious book cover with dark shadows, silhouette of a detective, foggy street scene, noir atmosphere',
|
||||
genre: t('coverDesigner.aiGenerator.mystery'),
|
||||
prompt: t('coverDesigner.aiGenerator.mysteryPrompt'),
|
||||
},
|
||||
{
|
||||
genre: 'Romance',
|
||||
prompt: 'A romantic book cover with soft pastel colors, couple silhouette at sunset, dreamy and emotional',
|
||||
genre: t('coverDesigner.aiGenerator.romance'),
|
||||
prompt: t('coverDesigner.aiGenerator.romancePrompt'),
|
||||
},
|
||||
{
|
||||
genre: 'Fantasy',
|
||||
prompt: 'An epic fantasy book cover with magical elements, dragon, castle in the background, mystical glowing effects',
|
||||
genre: t('coverDesigner.aiGenerator.fantasy'),
|
||||
prompt: t('coverDesigner.aiGenerator.fantasyPrompt'),
|
||||
},
|
||||
{
|
||||
genre: 'Sci-Fi',
|
||||
prompt: 'A futuristic sci-fi book cover with spaceships, neon lights, cyberpunk city, high-tech atmosphere',
|
||||
genre: t('coverDesigner.aiGenerator.scifi'),
|
||||
prompt: t('coverDesigner.aiGenerator.scifiPrompt'),
|
||||
},
|
||||
{
|
||||
genre: 'Self-Help',
|
||||
prompt: 'A clean self-help book cover with inspiring imagery, mountain peak or sunrise, professional and motivating',
|
||||
genre: t('coverDesigner.aiGenerator.selfhelp'),
|
||||
prompt: t('coverDesigner.aiGenerator.selfhelpPrompt'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -179,12 +183,12 @@ function AIGenerator({
|
||||
<div className="card space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
AI Cover Generator
|
||||
{t('coverDesigner.aiGenerator.title')}
|
||||
</h3>
|
||||
|
||||
{/* Style Selection */}
|
||||
<div className="mb-6">
|
||||
<label className="label">Select Style</label>
|
||||
<label className="label">{t('coverDesigner.aiGenerator.selectStyle')}</label>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2 mt-2">
|
||||
{styles.map((style) => (
|
||||
<button
|
||||
@@ -205,18 +209,18 @@ function AIGenerator({
|
||||
|
||||
{/* Prompt Input */}
|
||||
<div className="mb-6">
|
||||
<label className="label">Describe Your Cover</label>
|
||||
<label className="label">{t('coverDesigner.aiGenerator.describeCover')}</label>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Describe the book cover you want to generate... e.g., 'A mysterious forest with glowing eyes in the darkness'"
|
||||
placeholder={t('coverDesigner.aiGenerator.coverPlaceholder')}
|
||||
className="input min-h-[120px] resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Genre Presets */}
|
||||
<div className="mb-6">
|
||||
<label className="label">Quick Presets by Genre</label>
|
||||
<label className="label">{t('coverDesigner.aiGenerator.quickPresets')}</label>
|
||||
<div className="grid gap-2 mt-2">
|
||||
{genrePresets.map((preset) => (
|
||||
<button
|
||||
@@ -238,7 +242,7 @@ function AIGenerator({
|
||||
className="btn-primary w-full py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
<Wand2 className="w-5 h-5" />
|
||||
{isGenerating ? 'Generating...' : 'Generate Cover'}
|
||||
{isGenerating ? t('coverDesigner.aiGenerator.generating') : t('coverDesigner.aiGenerator.generateCover')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Stage, Layer, Rect, Text, Transformer, Image as KonvaImage } from 'reac
|
||||
import type Konva from 'konva';
|
||||
import { useCoverStore } from '../../stores/coverStore';
|
||||
import { Type, Download } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BackgroundImage = ({ url, width, height }: { url: string; width: number; height: number }) => {
|
||||
const [image, setImage] = useState<HTMLImageElement | null>(null);
|
||||
@@ -21,13 +22,13 @@ const BackgroundImage = ({ url, width, height }: { url: string; width: number; h
|
||||
};
|
||||
|
||||
const FONTS = [
|
||||
{ name: 'Roboto', label: 'Modern (Roboto)' },
|
||||
{ name: 'Montserrat', label: 'Minimalist (Montserrat)' },
|
||||
{ name: 'Playfair Display', label: 'Elegant (Playfair Display)' },
|
||||
{ name: 'Bebas Neue', label: 'Bold (Bebas Neue)' },
|
||||
{ name: 'Pacifico', label: 'Playful (Pacifico)' },
|
||||
{ name: 'Georgia', label: 'Classic (Georgia)' },
|
||||
{ name: 'Arial', label: 'Standard (Arial)' },
|
||||
{ name: 'Roboto', labelKey: 'coverDesigner.modernFont' },
|
||||
{ name: 'Montserrat', labelKey: 'coverDesigner.minimalistFont' },
|
||||
{ name: 'Playfair Display', labelKey: 'coverDesigner.elegantFont' },
|
||||
{ name: 'Bebas Neue', labelKey: 'coverDesigner.boldFont' },
|
||||
{ name: 'Pacifico', labelKey: 'coverDesigner.playfulFont' },
|
||||
{ name: 'Georgia', labelKey: 'coverDesigner.classicFont' },
|
||||
{ name: 'Arial', labelKey: 'coverDesigner.standardFont' },
|
||||
];
|
||||
|
||||
export default function CoverEditor() {
|
||||
@@ -37,11 +38,12 @@ export default function CoverEditor() {
|
||||
const [textTool, setTextTool] = useState<'title' | 'author'>('title');
|
||||
const [newText, setNewText] = useState('');
|
||||
const [selectedFont, setSelectedFont] = useState('Roboto');
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!activeCover) {
|
||||
return (
|
||||
<div className="card text-center py-12">
|
||||
<p className="text-gray-500">Select a cover from the gallery to edit</p>
|
||||
<p className="text-gray-500">{t('coverDesigner.selectCover')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -86,7 +88,7 @@ export default function CoverEditor() {
|
||||
<div className="lg:col-span-3 card p-4 bg-gray-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
Editing: {activeCover.title}
|
||||
{t('coverDesigner.editing')}: {activeCover.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -94,7 +96,7 @@ export default function CoverEditor() {
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Export
|
||||
{t('coverDesigner.export')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,12 +187,12 @@ export default function CoverEditor() {
|
||||
<div className="card">
|
||||
<h4 className="font-medium text-gray-900 mb-4 flex items-center gap-2">
|
||||
<Type className="w-4 h-4" />
|
||||
Add Text
|
||||
{t('coverDesigner.addText')}
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="label">Text Type</label>
|
||||
<label className="label">{t('coverDesigner.textType')}</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setTextTool('title')}
|
||||
@@ -200,7 +202,7 @@ export default function CoverEditor() {
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Title
|
||||
{t('coverDesigner.titleText')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTextTool('author')}
|
||||
@@ -210,13 +212,13 @@ export default function CoverEditor() {
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Author
|
||||
{t('coverDesigner.authorText')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Font</label>
|
||||
<label className="label">{t('coverDesigner.font')}</label>
|
||||
<select
|
||||
value={selectedFont}
|
||||
onChange={(e) => setSelectedFont(e.target.value)}
|
||||
@@ -224,19 +226,19 @@ export default function CoverEditor() {
|
||||
>
|
||||
{FONTS.map((font) => (
|
||||
<option key={font.name} value={font.name}>
|
||||
{font.label}
|
||||
{t(font.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Text Content</label>
|
||||
<label className="label">{t('coverDesigner.textContent')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newText}
|
||||
onChange={(e) => setNewText(e.target.value)}
|
||||
placeholder={textTool === 'title' ? 'Book Title' : 'Author Name'}
|
||||
placeholder={textTool === 'title' ? t('coverDesigner.bookTitlePlaceholder') : t('coverDesigner.authorNamePlaceholder')}
|
||||
className="input"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddText()}
|
||||
/>
|
||||
@@ -247,7 +249,7 @@ export default function CoverEditor() {
|
||||
disabled={!newText.trim()}
|
||||
className="btn-primary w-full"
|
||||
>
|
||||
Add Text Layer
|
||||
{t('coverDesigner.addTextLayer')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +271,7 @@ export default function CoverEditor() {
|
||||
>
|
||||
{FONTS.map((font) => (
|
||||
<option key={font.name} value={font.name}>
|
||||
{font.label}
|
||||
{t(font.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Image, Eye, Trash2, Download } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CoverData {
|
||||
id: string;
|
||||
@@ -14,13 +15,15 @@ interface CoverGalleryProps {
|
||||
}
|
||||
|
||||
export default function CoverGallery({ covers, onSelect, onDelete }: CoverGalleryProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (covers.length === 0) {
|
||||
return (
|
||||
<div className="card text-center py-12">
|
||||
<Image className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">No covers yet</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900">{t('coverDesigner.noCovers')}</h3>
|
||||
<p className="text-gray-500 mt-2">
|
||||
Upload a cover image or generate one with AI to get started
|
||||
{t('coverDesigner.noCoversDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -43,7 +46,7 @@ export default function CoverGallery({ covers, onSelect, onDelete }: CoverGaller
|
||||
<button
|
||||
onClick={() => onSelect(cover.url)}
|
||||
className="p-2 bg-white rounded-lg hover:bg-gray-100 transition-colors"
|
||||
title="Edit"
|
||||
title={t('coverDesigner.edit')}
|
||||
>
|
||||
<Eye className="w-5 h-5 text-gray-700" />
|
||||
</button>
|
||||
@@ -55,14 +58,14 @@ export default function CoverGallery({ covers, onSelect, onDelete }: CoverGaller
|
||||
link.click();
|
||||
}}
|
||||
className="p-2 bg-white rounded-lg hover:bg-gray-100 transition-colors"
|
||||
title="Download"
|
||||
title={t('coverDesigner.download')}
|
||||
>
|
||||
<Download className="w-5 h-5 text-gray-700" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(cover.id)}
|
||||
className="p-2 bg-red-500 rounded-lg hover:bg-red-600 transition-colors"
|
||||
title="Delete"
|
||||
title={t('coverDesigner.delete')}
|
||||
>
|
||||
<Trash2 className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
|
||||
@@ -35,3 +35,71 @@ body {
|
||||
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||
}
|
||||
}
|
||||
|
||||
/* TipTap WYSIWYG Editor Styles */
|
||||
.tiptap {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.tiptap p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #adb5bd;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Book chapter formatting */
|
||||
.tiptap p {
|
||||
text-indent: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.tiptap p:first-child {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.tiptap h1, .tiptap h2, .tiptap h3 {
|
||||
text-align: center;
|
||||
text-indent: 0;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tiptap blockquote {
|
||||
text-align: left;
|
||||
text-indent: 0;
|
||||
margin-left: 2rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.tiptap ul, .tiptap ol {
|
||||
text-align: left;
|
||||
text-indent: 0;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.tiptap h1 { font-size: 2em; font-weight: 700; margin: 0.67em 0; }
|
||||
.tiptap h2 { font-size: 1.5em; font-weight: 600; margin: 0.75em 0; }
|
||||
.tiptap h3 { font-size: 1.25em; font-weight: 600; margin: 0.83em 0; }
|
||||
|
||||
.tiptap ul { list-style: disc; padding-left: 1.5em; margin: 0.5em 0; }
|
||||
.tiptap ol { list-style: decimal; padding-left: 1.5em; margin: 0.5em 0; }
|
||||
.tiptap li { margin: 0.25em 0; }
|
||||
|
||||
.tiptap blockquote {
|
||||
border-left: 3px solid #e5e7eb;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tiptap hr {
|
||||
border: none;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
.tiptap p { margin: 0.5em 0; }
|
||||
|
||||
@@ -24,5 +24,148 @@
|
||||
"languageSelect": "Select your preferred language",
|
||||
"english": "English",
|
||||
"spanish": "Español"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Welcome to CreaBook",
|
||||
"subtitle": "Let's personalize your creative journey.",
|
||||
"step1": "1. Select your language",
|
||||
"step2": "2. What kind of book do you want to create?",
|
||||
"ideaPlaceholder": "E.g., A sci-fi thriller about a detective on Mars...",
|
||||
"genreLabel": "Genre (optional)",
|
||||
"step3": "3. How do you want to start?",
|
||||
"btnFree": "Move Freely (Blank Canvas)",
|
||||
"btnAI": "Use AI to Generate Outline"
|
||||
},
|
||||
"genres": {
|
||||
"fiction": { "name": "Fiction", "description": "General literary fiction with focus on character development and narrative" },
|
||||
"mystery": { "name": "Mystery", "description": "Puzzle-driven narratives with clues, suspects, and a satisfying reveal" },
|
||||
"romance": { "name": "Romance", "description": "Love-centered stories with emotional intimacy and satisfying relationship resolution" },
|
||||
"scifi": { "name": "Science Fiction", "description": "Speculative fiction exploring technology, space, time, and their impact on humanity" },
|
||||
"fantasy": { "name": "Fantasy", "description": "Magical worlds with supernatural elements, quests, and epic stakes" },
|
||||
"horror": { "name": "Horror", "description": "Fear-driven narratives designed to unsettle, frighten, and provoke dread" },
|
||||
"thriller": { "name": "Thriller", "description": "High-stakes, fast-paced narratives with constant tension and danger" },
|
||||
"children": { "name": "Children's Book", "description": "Age-appropriate stories with clear morals, simple language, and engaging characters" },
|
||||
"nonfiction": { "name": "Non-Fiction", "description": "Factual, informative content organized around a central topic or argument" },
|
||||
"selfhelp": { "name": "Self-Help", "description": "Practical guidance for personal improvement and growth" },
|
||||
"business": { "name": "Business", "description": "Professional insights, strategies, and case studies for business success" },
|
||||
"memoir": { "name": "Memoir", "description": "Personal life stories focused on transformation and universal themes" }
|
||||
},
|
||||
"genreSelector": {
|
||||
"selectGenre": "Select Your Genre",
|
||||
"bookDetails": "Book Details",
|
||||
"bookTitle": "Book Title (optional)",
|
||||
"bookTitlePlaceholder": "My Amazing Book",
|
||||
"coreIdea": "Core Idea",
|
||||
"coreIdeaPlaceholder": "Describe your book idea... What's the story about? Who are the main characters? What conflict drives the narrative?",
|
||||
"tip": "The more details you provide, the better the AI can generate your outline and content.",
|
||||
"generateOutline": "Generate Outline"
|
||||
},
|
||||
"bookGenerator": {
|
||||
"title": "Book Generator",
|
||||
"subtitle": "Generate book ideas and write with AI assistance",
|
||||
"tabGenre": "Genre",
|
||||
"tabOutline": "Outline",
|
||||
"tabWrite": "Write",
|
||||
"tabCharacters": "Characters"
|
||||
},
|
||||
"bookEditor": {
|
||||
"chapters": "Chapters",
|
||||
"written": "Written",
|
||||
"chapterSummary": "Chapter Summary",
|
||||
"chapterOf": "Chapter {{current}} of {{total}}",
|
||||
"aiAssist": "AI Assist",
|
||||
"generating": "Generating...",
|
||||
"generateChapter": "Generate Chapter",
|
||||
"aiWritingAssistant": "AI Writing Assistant",
|
||||
"expandSection": "Expand this section",
|
||||
"improveProse": "Improve prose",
|
||||
"addDescription": "Add description",
|
||||
"rewriteParagraph": "Rewrite paragraph",
|
||||
"words": "words",
|
||||
"lastUpdated": "Last updated",
|
||||
"noOutline": "Generate an outline first to start writing your book.",
|
||||
"editorPlaceholder": "Start writing or use AI to generate this chapter...",
|
||||
"written": "Written"
|
||||
},
|
||||
"bookOutline": {
|
||||
"untitledBook": "Untitled Book",
|
||||
"genre": "Genre",
|
||||
"generatingOutline": "Generating...",
|
||||
"generateOutline": "Generate Outline",
|
||||
"generatedOutline": "Generated Outline",
|
||||
"logline": "Logline",
|
||||
"chapterOutline": "Chapter Outline",
|
||||
"chapter": "Chapter",
|
||||
"noSummary": "No summary available",
|
||||
"readyToWrite": "Ready to start writing! Navigate to the Write tab to begin.",
|
||||
"readyToGenerate": "Ready to Generate",
|
||||
"readyToGenerateDesc": "Click \"Generate Outline\" to create a detailed chapter outline based on your genre and book idea. The AI will create a structure following genre-specific patterns.",
|
||||
"failedToGenerate": "Failed to generate outline"
|
||||
},
|
||||
"characters": {
|
||||
"title": "Character Development",
|
||||
"generating": "Generating...",
|
||||
"generate": "Generate Characters",
|
||||
"empty": "No characters yet. Click \"Generate Characters\" to create AI-suggested characters.",
|
||||
"traits": "Traits",
|
||||
"motivation": "Motivation"
|
||||
},
|
||||
"coverDesigner": {
|
||||
"title": "Cover Designer",
|
||||
"subtitle": "Design stunning book covers with AI or manual editing",
|
||||
"tabGallery": "Gallery",
|
||||
"tabEditor": "Editor",
|
||||
"tabAIGenerate": "AI Generate",
|
||||
"newBookTitle": "New Book",
|
||||
"authorName": "Author Name",
|
||||
"noCovers": "No covers yet",
|
||||
"noCoversDesc": "Upload a cover image or generate one with AI to get started",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"selectCover": "Select a cover from the gallery to edit",
|
||||
"editing": "Editing",
|
||||
"export": "Export",
|
||||
"addText": "Add Text",
|
||||
"textType": "Text Type",
|
||||
"titleText": "Title",
|
||||
"authorText": "Author",
|
||||
"font": "Font",
|
||||
"modernFont": "Modern (Roboto)",
|
||||
"minimalistFont": "Minimalist (Montserrat)",
|
||||
"elegantFont": "Elegant (Playfair Display)",
|
||||
"boldFont": "Bold (Bebas Neue)",
|
||||
"playfulFont": "Playful (Pacifico)",
|
||||
"classicFont": "Classic (Georgia)",
|
||||
"standardFont": "Standard (Arial)",
|
||||
"textContent": "Text Content",
|
||||
"bookTitlePlaceholder": "Book Title",
|
||||
"authorNamePlaceholder": "Author Name",
|
||||
"addTextLayer": "Add Text Layer",
|
||||
"aiGenerator": {
|
||||
"title": "AI Cover Generator",
|
||||
"selectStyle": "Select Style",
|
||||
"modern": "Modern",
|
||||
"minimalist": "Minimalist",
|
||||
"vintage": "Vintage",
|
||||
"bold": "Bold",
|
||||
"elegant": "Elegant",
|
||||
"playful": "Playful",
|
||||
"describeCover": "Describe Your Cover",
|
||||
"coverPlaceholder": "Describe the book cover you want to generate... e.g., 'A mysterious forest with glowing eyes in the darkness'",
|
||||
"quickPresets": "Quick Presets by Genre",
|
||||
"mystery": "Mystery",
|
||||
"mysteryPrompt": "A mysterious book cover with dark shadows, silhouette of a detective, foggy street scene, noir atmosphere",
|
||||
"romance": "Romance",
|
||||
"romancePrompt": "A romantic book cover with soft pastel colors, couple silhouette at sunset, dreamy and emotional",
|
||||
"fantasy": "Fantasy",
|
||||
"fantasyPrompt": "An epic fantasy book cover with magical elements, dragon, castle in the background, mystical glowing effects",
|
||||
"scifi": "Sci-Fi",
|
||||
"scifiPrompt": "A futuristic sci-fi book cover with spaceships, neon lights, cyberpunk city, high-tech atmosphere",
|
||||
"selfhelp": "Self-Help",
|
||||
"selfhelpPrompt": "A clean self-help book cover with inspiring imagery, mountain peak or sunrise, professional and motivating",
|
||||
"generating": "Generating...",
|
||||
"generateCover": "Generate Cover"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,5 +24,148 @@
|
||||
"languageSelect": "Selecciona tu idioma preferido",
|
||||
"english": "Inglés",
|
||||
"spanish": "Español"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Bienvenido a CreaBook",
|
||||
"subtitle": "Vamos a personalizar tu viaje creativo.",
|
||||
"step1": "1. Selecciona tu idioma",
|
||||
"step2": "2. ¿Qué tipo de libro quieres crear?",
|
||||
"ideaPlaceholder": "Ej. Un thriller de ciencia ficción sobre un detective en Marte...",
|
||||
"genreLabel": "Género (opcional)",
|
||||
"step3": "3. ¿Cómo quieres empezar?",
|
||||
"btnFree": "Moverse libremente (Lienzo en blanco)",
|
||||
"btnAI": "Usar IA para generar tu esquema"
|
||||
},
|
||||
"genres": {
|
||||
"fiction": { "name": "Ficción", "description": "Ficción literaria general centrada en el desarrollo de personajes y la narrativa" },
|
||||
"mystery": { "name": "Misterio", "description": "Narrativas basadas en enigmas con pistas, sospechosos y una revelación satisfactoria" },
|
||||
"romance": { "name": "Romance", "description": "Historias centradas en el amor con intimidad emocional y una resolución satisfactoria" },
|
||||
"scifi": { "name": "Ciencia Ficción", "description": "Ficción especulativa que explora la tecnología, el espacio, el tiempo y su impacto en la humanidad" },
|
||||
"fantasy": { "name": "Fantasía", "description": "Mundos mágicos con elementos sobrenaturales, misiones y grandes desafíos" },
|
||||
"horror": { "name": "Terror", "description": "Narrativas de miedo diseñadas para inquietar, asustar y provocar pavor" },
|
||||
"thriller": { "name": "Thriller", "description": "Narrativas de alto riesgo y ritmo rápido con tensión y peligro constantes" },
|
||||
"children": { "name": "Libro Infantil", "description": "Historias apropiadas para niños con moralejas claras, lenguaje sencillo y personajes atractivos" },
|
||||
"nonfiction": { "name": "No Ficción", "description": "Contenido informativo y factual organizado en torno a un tema o argumento central" },
|
||||
"selfhelp": { "name": "Autoayuda", "description": "Guía práctica para la mejora y el crecimiento personal" },
|
||||
"business": { "name": "Negocios", "description": "Perspectivas profesionales, estrategias y casos de estudio para el éxito empresarial" },
|
||||
"memoir": { "name": "Memorias", "description": "Historias de vida personales centradas en la transformación y temas universales" }
|
||||
},
|
||||
"genreSelector": {
|
||||
"selectGenre": "Selecciona tu Género",
|
||||
"bookDetails": "Detalles del Libro",
|
||||
"bookTitle": "Título del Libro (opcional)",
|
||||
"bookTitlePlaceholder": "Mi Libro Increíble",
|
||||
"coreIdea": "Idea Principal",
|
||||
"coreIdeaPlaceholder": "Describe tu idea para el libro... ¿De qué trata la historia? ¿Quiénes son los personajes principales? ¿Qué conflicto impulsa la narrativa?",
|
||||
"tip": "Cuantos más detalles proporciones, mejor podrá la IA generar tu esquema y contenido.",
|
||||
"generateOutline": "Generar Esquema"
|
||||
},
|
||||
"bookGenerator": {
|
||||
"title": "Generador de Libros",
|
||||
"subtitle": "Genera ideas de libros y escribe con asistencia de IA",
|
||||
"tabGenre": "Género",
|
||||
"tabOutline": "Esquema",
|
||||
"tabWrite": "Escribir",
|
||||
"tabCharacters": "Personajes"
|
||||
},
|
||||
"bookEditor": {
|
||||
"chapters": "Capítulos",
|
||||
"written": "Escrito",
|
||||
"chapterSummary": "Resumen del Capítulo",
|
||||
"chapterOf": "Capítulo {{current}} de {{total}}",
|
||||
"aiAssist": "Asistente IA",
|
||||
"generating": "Generando...",
|
||||
"generateChapter": "Generar Capítulo",
|
||||
"aiWritingAssistant": "Asistente de Escritura IA",
|
||||
"expandSection": "Expandir esta sección",
|
||||
"improveProse": "Mejorar prosa",
|
||||
"addDescription": "Añadir descripción",
|
||||
"rewriteParagraph": "Reescribir párrafo",
|
||||
"words": "palabras",
|
||||
"lastUpdated": "Última actualización",
|
||||
"noOutline": "Genera un esquema primero para empezar a escribir tu libro.",
|
||||
"editorPlaceholder": "Empieza a escribir o usa la IA para generar este capítulo...",
|
||||
"written": "Escrito"
|
||||
},
|
||||
"bookOutline": {
|
||||
"untitledBook": "Libro sin título",
|
||||
"genre": "Género",
|
||||
"generatingOutline": "Generando...",
|
||||
"generateOutline": "Generar Esquema",
|
||||
"generatedOutline": "Esquema Generado",
|
||||
"logline": "Sinopsis",
|
||||
"chapterOutline": "Esquema de Capítulos",
|
||||
"chapter": "Capítulo",
|
||||
"noSummary": "Sin resumen disponible",
|
||||
"readyToWrite": "¡Listo para escribir! Navega a la pestaña Escribir para comenzar.",
|
||||
"readyToGenerate": "Listo para Generar",
|
||||
"readyToGenerateDesc": "Haz clic en \"Generar Esquema\" para crear un esquema detallado de capítulos basado en tu género e idea. La IA creará una estructura siguiendo patrones específicos del género.",
|
||||
"failedToGenerate": "Error al generar el esquema"
|
||||
},
|
||||
"characters": {
|
||||
"title": "Desarrollo de Personajes",
|
||||
"generating": "Generando...",
|
||||
"generate": "Generar Personajes",
|
||||
"empty": "Aún no hay personajes. Haz clic en \"Generar Personajes\" para crear personajes sugeridos por la IA.",
|
||||
"traits": "Rasgos",
|
||||
"motivation": "Motivación"
|
||||
},
|
||||
"coverDesigner": {
|
||||
"title": "Diseñador de Portadas",
|
||||
"subtitle": "Diseña portadas de libros impresionantes con IA o edición manual",
|
||||
"tabGallery": "Galería",
|
||||
"tabEditor": "Editor",
|
||||
"tabAIGenerate": "Generar con IA",
|
||||
"newBookTitle": "Nuevo Libro",
|
||||
"authorName": "Nombre del Autor",
|
||||
"noCovers": "Aún no hay portadas",
|
||||
"noCoversDesc": "Sube una imagen de portada o genera una con IA para comenzar",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"download": "Descargar",
|
||||
"selectCover": "Selecciona una portada de la galería para editar",
|
||||
"editing": "Editando",
|
||||
"export": "Exportar",
|
||||
"addText": "Añadir Texto",
|
||||
"textType": "Tipo de Texto",
|
||||
"titleText": "Título",
|
||||
"authorText": "Autor",
|
||||
"font": "Fuente",
|
||||
"modernFont": "Moderno (Roboto)",
|
||||
"minimalistFont": "Minimalista (Montserrat)",
|
||||
"elegantFont": "Elegante (Playfair Display)",
|
||||
"boldFont": "Audaz (Bebas Neue)",
|
||||
"playfulFont": "Juguetón (Pacifico)",
|
||||
"classicFont": "Clásico (Georgia)",
|
||||
"standardFont": "Estándar (Arial)",
|
||||
"textContent": "Contenido del Texto",
|
||||
"bookTitlePlaceholder": "Título del Libro",
|
||||
"authorNamePlaceholder": "Nombre del Autor",
|
||||
"addTextLayer": "Añadir Capa de Texto",
|
||||
"aiGenerator": {
|
||||
"title": "Generador de Portadas con IA",
|
||||
"selectStyle": "Seleccionar Estilo",
|
||||
"modern": "Moderno",
|
||||
"minimalist": "Minimalista",
|
||||
"vintage": "Antiguo",
|
||||
"bold": "Audaz",
|
||||
"elegant": "Elegante",
|
||||
"playful": "Juguetón",
|
||||
"describeCover": "Describe tu Portada",
|
||||
"coverPlaceholder": "Describe la portada del libro que quieres generar... ej. 'Un bosque misterioso con ojos brillantes en la oscuridad'",
|
||||
"quickPresets": "Preajustes Rápidos por Género",
|
||||
"mystery": "Misterio",
|
||||
"mysteryPrompt": "Una portada de libro misteriosa con sombras oscuras, silueta de un detective, escena de calle brumosa, atmósfera noir",
|
||||
"romance": "Romance",
|
||||
"romancePrompt": "Una portada de libro romántica con colores pastel suaves, silueta de pareja al atardecer, soñadora y emocional",
|
||||
"fantasy": "Fantasía",
|
||||
"fantasyPrompt": "Una portada de libro de fantasía épica con elementos mágicos, dragón, castillo al fondo, efectos luminosos místicos",
|
||||
"scifi": "Ciencia Ficción",
|
||||
"scifiPrompt": "Una portada de libro de ciencia ficción futurista con naves espaciales, luces de neón, ciudad cyberpunk, atmósfera high-tech",
|
||||
"selfhelp": "Autoayuda",
|
||||
"selfhelpPrompt": "Una portada de libro de autoayuda limpia con imágenes inspiradoras, cima de montaña o amanecer, profesional y motivadora",
|
||||
"generating": "Generando...",
|
||||
"generateCover": "Generar Portada"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ export interface BookOutline {
|
||||
genre: string;
|
||||
logline: string;
|
||||
chapters: Chapter[];
|
||||
// Error handling properties
|
||||
error?: string;
|
||||
raw?: string;
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
|
||||
Reference in New Issue
Block a user