feat: Implement writing streak tracking, add chapter API routes, and introduce a BookSettings component with i18n support.

This commit is contained in:
2026-03-06 11:28:57 +01:00
parent 0c4bb60dce
commit 893560737a
9 changed files with 213 additions and 52 deletions

View File

@@ -3,7 +3,7 @@
import React, { useEffect, useState } from 'react';
import { BookProject, BookSettings } from '@/lib/types';
import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants';
import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react';
import { Settings, Book, Feather, Users, Clock, Target, Hash, Save, Check } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations';
@@ -26,37 +26,73 @@ const DEFAULT_SETTINGS: BookSettings = {
const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => {
const { t } = useLanguage();
const [settings, setSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
// Local state for all editable fields to prevent excessive API calls
const [localTitle, setLocalTitle] = useState(project.title);
const [localAuthor, setLocalAuthor] = useState(project.author);
const [localStyleGuide, setLocalStyleGuide] = useState(project.styleGuide || '');
const [localSettings, setLocalSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showSavedFeedback, setShowSavedFeedback] = useState(false);
useEffect(() => {
setLocalTitle(project.title);
setLocalAuthor(project.author);
setLocalStyleGuide(project.styleGuide || '');
if (project.settings) {
setSettings(project.settings);
setLocalSettings(project.settings);
}
}, [project.settings]);
}, [project.title, project.author, project.styleGuide, project.settings]);
const handleChange = (key: keyof BookSettings, value: string) => {
const newSettings = { ...settings, [key]: value };
setSettings(newSettings);
onUpdate({ ...project, settings: newSettings });
setLocalSettings(prev => ({ ...prev, [key]: value }));
};
const handleStyleGuideChange = (value: string) => {
onUpdate({ ...project, styleGuide: value });
const handleSave = () => {
setIsSaving(true);
onUpdate({
...project,
title: localTitle,
author: localAuthor,
styleGuide: localStyleGuide,
settings: localSettings
});
// Simulate save delay for UI feedback
setTimeout(() => {
setIsSaving(false);
setShowSavedFeedback(true);
setTimeout(() => setShowSavedFeedback(false), 2000);
}, 500);
};
return (
<div className="h-full bg-theme-bg p-8 overflow-y-auto transition-colors duration-300">
<div className="max-w-4xl mx-auto bg-theme-panel rounded-xl shadow-lg border border-theme-border overflow-hidden transition-colors duration-300">
<div className="max-w-4xl mx-auto bg-theme-panel rounded-xl shadow-lg border border-theme-border overflow-hidden transition-colors duration-300 pb-8">
<div className="bg-slate-900 text-white p-6 border-b border-slate-800 flex items-center gap-4">
<div className="bg-blue-600 p-3 rounded-lg">
<Settings size={24} />
</div>
<div>
<h2 className="text-2xl font-bold">{t('book_settings.title')}</h2>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p>
<div className="bg-slate-900 text-white p-6 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="bg-blue-600 p-3 rounded-lg">
<Settings size={24} />
</div>
<div>
<h2 className="text-2xl font-bold">{t('book_settings.title')}</h2>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p>
</div>
</div>
<button
onClick={handleSave}
disabled={isSaving}
className={`flex items-center gap-2 px-6 py-2.5 rounded-lg font-bold transition-all ${showSavedFeedback
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{showSavedFeedback ? <Check size={18} /> : <Save size={18} />}
{showSavedFeedback ? t('book_settings.saved' as TranslationKey) || 'Sauvegardé' : t('book_settings.save' as TranslationKey) || 'Sauvegarder'}
</button>
</div>
<div className="p-8 space-y-8">
@@ -69,8 +105,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.novel_title')}</label>
<input
type="text"
value={project.title}
onChange={(e) => onUpdate({ ...project, title: e.target.value })}
value={localTitle}
onChange={(e) => setLocalTitle(e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-serif font-bold text-lg transition-colors duration-300"
/>
</div>
@@ -78,8 +114,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.author_name')}</label>
<input
type="text"
value={project.author}
onChange={(e) => onUpdate({ ...project, author: e.target.value })}
value={localAuthor}
onChange={(e) => setLocalAuthor(e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
/>
</div>
@@ -87,7 +123,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<div>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label>
<textarea
value={settings.synopsis}
value={localSettings.synopsis}
onChange={(e) => handleChange('synopsis', e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none h-24 text-sm transition-colors duration-300"
placeholder={t('book_settings.synopsis_placeholder')}
@@ -105,7 +141,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<input
type="text"
list="genre-suggestions"
value={settings.genre}
value={localSettings.genre}
onChange={(e) => handleChange('genre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.genre_placeholder')}
@@ -118,7 +154,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.sub_genre')}</label>
<input
type="text"
value={settings.subGenre || ''}
value={localSettings.subGenre || ''}
onChange={(e) => handleChange('subGenre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.subgenre_placeholder')}
@@ -128,7 +164,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.target_audience')}</label>
<input
type="text"
value={settings.targetAudience}
value={localSettings.targetAudience}
onChange={(e) => handleChange('targetAudience', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.audience_placeholder')}
@@ -141,7 +177,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Hash size={14} className="absolute left-3 top-3 text-theme-muted" />
<input
type="text"
value={settings.themes}
value={localSettings.themes}
onChange={(e) => handleChange('themes', e.target.value)}
className="w-full pl-9 p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.themes_placeholder')}
@@ -160,7 +196,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Users size={14} /> {t('book_settings.pov')}
</label>
<select
value={settings.pov}
value={localSettings.pov}
onChange={(e) => handleChange('pov', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
>
@@ -173,7 +209,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Clock size={14} /> {t('book_settings.tense')}
</label>
<select
value={settings.tense}
value={localSettings.tense}
onChange={(e) => handleChange('tense', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
>
@@ -186,7 +222,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<input
type="text"
list="tone-suggestions"
value={settings.tone}
value={localSettings.tone}
onChange={(e) => handleChange('tone', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.tone_placeholder')}
@@ -205,8 +241,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
{t('book_settings.style_guide_help')}
</p>
<textarea
value={project.styleGuide || ''}
onChange={(e) => handleStyleGuideChange(e.target.value)}
value={localStyleGuide}
onChange={(e) => setLocalStyleGuide(e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 text-sm font-mono transition-colors duration-300"
placeholder={t('book_settings.style_guide_placeholder')}
/>