Multi lingua, hotfixes and design

This commit is contained in:
Ichitux
2026-04-05 03:56:31 +02:00
parent 594b50b77f
commit 6cd1bf305d
29 changed files with 3157 additions and 562 deletions

View File

@@ -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 />} />

View 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>
);
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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; }

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -13,6 +13,9 @@ export interface BookOutline {
genre: string;
logline: string;
chapters: Chapter[];
// Error handling properties
error?: string;
raw?: string;
}
export interface Character {