diff --git a/.next/dev/cache/turbopack/23c46498/CURRENT b/.next/dev/cache/turbopack/23c46498/CURRENT index 187cfc9..8ddf084 100644 Binary files a/.next/dev/cache/turbopack/23c46498/CURRENT and b/.next/dev/cache/turbopack/23c46498/CURRENT differ diff --git a/.next/dev/cache/turbopack/23c46498/LOG b/.next/dev/cache/turbopack/23c46498/LOG index 5538d19..9d93da8 100644 --- a/.next/dev/cache/turbopack/23c46498/LOG +++ b/.next/dev/cache/turbopack/23c46498/LOG @@ -6122,3 +6122,9 @@ FAM | META SEQ | SST SEQ | RANGE 0 | 00014933 | 00014932 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) 1 | 00014934 | 00014930 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) 2 | 00014935 | 00014931 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) +Time 2026-03-06T10:28:56.8886436Z +Commit 00015705 4 keys in 7ms 974µs 900ns +FAM | META SEQ | SST SEQ | RANGE + 0 | 00015703 | 00015702 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) + 1 | 00015704 | 00015700 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) + 2 | 00015705 | 00015701 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/api/chapters/[id]/route.ts b/src/app/api/chapters/[id]/route.ts index 8df77fa..1f06f66 100644 --- a/src/app/api/chapters/[id]/route.ts +++ b/src/app/api/chapters/[id]/route.ts @@ -36,6 +36,13 @@ export async function PUT( }, }); + // Update writing streak if content was explicitly updated + if (body.content !== undefined) { + import('@/lib/streak').then(({ updateWritingStreak }) => { + updateWritingStreak(session.user.id).catch(console.error); + }); + } + return NextResponse.json(updated); } diff --git a/src/app/api/chapters/route.ts b/src/app/api/chapters/route.ts index 943e7ca..70e183a 100644 --- a/src/app/api/chapters/route.ts +++ b/src/app/api/chapters/route.ts @@ -34,5 +34,10 @@ export async function POST(request: NextRequest) { }, }); + // Update writing streak + import('@/lib/streak').then(({ updateWritingStreak }) => { + updateWritingStreak(session.user.id).catch(console.error); + }); + return NextResponse.json(chapter, { status: 201 }); } \ No newline at end of file diff --git a/src/components/BookSettings.tsx b/src/components/BookSettings.tsx index 563d414..0fc2451 100644 --- a/src/components/BookSettings.tsx +++ b/src/components/BookSettings.tsx @@ -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 = ({ project, onUpdate, onDeleteProject }) => { const { t } = useLanguage(); - const [settings, setSettings] = useState(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(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 ( - + - - - - - - {t('book_settings.title')} - {t('book_settings.subtitle')} + + + + + + + {t('book_settings.title')} + {t('book_settings.subtitle')} + + + {showSavedFeedback ? : } + {showSavedFeedback ? t('book_settings.saved' as TranslationKey) || 'Sauvegardé' : t('book_settings.save' as TranslationKey) || 'Sauvegarder'} + @@ -69,8 +105,8 @@ const BookSettingsComponent: React.FC = ({ project, onUpdate, {t('book_settings.novel_title')} 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" /> @@ -78,8 +114,8 @@ const BookSettingsComponent: React.FC = ({ project, onUpdate, {t('book_settings.author_name')} 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" /> @@ -87,7 +123,7 @@ const BookSettingsComponent: React.FC = ({ project, onUpdate, {t('book_settings.global_synopsis')} 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 = ({ project, onUpdate, 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 = ({ project, onUpdate, {t('book_settings.sub_genre')} 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 = ({ project, onUpdate, {t('book_settings.target_audience')} 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 = ({ project, onUpdate, 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 = ({ project, onUpdate, {t('book_settings.pov')} 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 = ({ project, onUpdate, {t('book_settings.tense')} 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 = ({ project, onUpdate, 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 = ({ project, onUpdate, {t('book_settings.style_guide_help')} 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')} /> diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index 9613842..0aca025 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -94,6 +94,8 @@ export const translations = { 'book_settings.confirm_delete': 'Oui, supprimer définitivement', 'book_settings.cancel': 'Annuler', 'book_settings.delete_button': 'Supprimer ce projet', + 'book_settings.save': 'Sauvegarder', + 'book_settings.saved': 'Sauvegardé', // Landing Page 'landing.nav_features': 'Fonctionnalités', @@ -323,11 +325,12 @@ export const translations = { 'sw.save_color': '+ SAUVER', // Genre, Tense Constants Translation Setup - 'pov_options.première_personne': 'Première Personne', - 'pov_options.troisième_personne_limitée': 'Troisième Personne Limitée', - 'pov_options.troisième_personne_omnisciente': 'Troisième Personne Omnisciente', - 'tense_options.présent': 'Présent', - 'tense_options.passé': 'Passé', + 'pov_options.1ère_personne_(je)': '1ère personne (Je)', + 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3ème personne (Limitée au protagoniste)', + 'pov_options.3ème_personne_(omnisciente)': '3ème personne (Omnisciente)', + 'pov_options.multi-points_de_vue_(alterné)': 'Multi-points de vue (Alterné)', + 'tense_options.passé_(passé_simple_/_imparfait)': 'Passé (Passé simple / Imparfait)', + 'tense_options.présent_de_narration': 'Présent de narration', }, en: { // General Navigation @@ -422,6 +425,8 @@ export const translations = { 'book_settings.confirm_delete': 'Yes, delete permanently', 'book_settings.cancel': 'Cancel', 'book_settings.delete_button': 'Delete this project', + 'book_settings.save': 'Save', + 'book_settings.saved': 'Saved', // Landing Page 'landing.nav_features': 'Features', @@ -651,11 +656,12 @@ export const translations = { 'sw.save_color': '+ SAVE', // Genre, Tense Constants Translation Setup - 'pov_options.première_personne': 'First Person', - 'pov_options.troisième_personne_limitée': 'Third Person Limited', - 'pov_options.troisième_personne_omnisciente': 'Third Person Omniscient', - 'tense_options.présent': 'Present', - 'tense_options.passé': 'Past', + 'pov_options.1ère_personne_(je)': '1st Person (I)', + 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3rd Person (Limited)', + 'pov_options.3ème_personne_(omnisciente)': '3rd Person (Omniscient)', + 'pov_options.multi-points_de_vue_(alterné)': 'Multi-POV (Alternating)', + 'tense_options.passé_(passé_simple_/_imparfait)': 'Past Tense', + 'tense_options.présent_de_narration': 'Present Tense', }, es: { // General Navigation @@ -750,6 +756,8 @@ export const translations = { 'book_settings.confirm_delete': 'Sí, eliminar permanentemente', 'book_settings.cancel': 'Cancelar', 'book_settings.delete_button': 'Eliminar este proyecto', + 'book_settings.save': 'Guardar', + 'book_settings.saved': 'Guardado', // Landing Page 'landing.nav_features': 'Características', @@ -979,11 +987,12 @@ export const translations = { 'sw.save_color': '+ GUARDAR', // Genre, Tense Constants Translation Setup - 'pov_options.première_personne': 'Primera Persona', - 'pov_options.troisième_personne_limitée': 'Tercera Persona Limitada', - 'pov_options.troisième_personne_omnisciente': 'Tercera Persona Omnisciente', - 'tense_options.présent': 'Presente', - 'tense_options.passé': 'Pasado', + 'pov_options.1ère_personne_(je)': '1ª Persona (Yo)', + 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3ª Persona (Limitada)', + 'pov_options.3ème_personne_(omnisciente)': '3ª Persona (Omnisciente)', + 'pov_options.multi-points_de_vue_(alterné)': 'Múltiples POV (Alternando)', + 'tense_options.passé_(passé_simple_/_imparfait)': 'Tiempo Pasado', + 'tense_options.présent_de_narration': 'Tiempo Presente', }, de: { // General Navigation @@ -1078,6 +1087,8 @@ export const translations = { 'book_settings.confirm_delete': 'Ja, dauerhaft löschen', 'book_settings.cancel': 'Abbrechen', 'book_settings.delete_button': 'Dieses Projekt löschen', + 'book_settings.save': 'Speichern', + 'book_settings.saved': 'Gespeichert', // Landing Page 'landing.nav_features': 'Funktionen', @@ -1307,11 +1318,12 @@ export const translations = { 'sw.save_color': '+ SPEICHERN', // Genre, Tense Constants Translation Setup - 'pov_options.première_personne': 'Ich-Perspektive', - 'pov_options.troisième_personne_limitée': 'Personale Erzählsituation', - 'pov_options.troisième_personne_omnisciente': 'Auktorialer Erzähler', - 'tense_options.présent': 'Präsens', - 'tense_options.passé': 'Präteritum', + 'pov_options.1ère_personne_(je)': 'Ich-Perspektive', + 'pov_options.3ème_personne_(limitée_au_protagoniste)': 'Er/Sie-Perspektive (personal)', + 'pov_options.3ème_personne_(omnisciente)': 'Er/Sie-Perspektive (auktorial)', + 'pov_options.multi-points_de_vue_(alterné)': 'Mehrere Perspektiven', + 'tense_options.passé_(passé_simple_/_imparfait)': 'Vergangenheit (Präteritum)', + 'tense_options.présent_de_narration': 'Gegenwart (Präsens)', } }; diff --git a/src/lib/streak.ts b/src/lib/streak.ts new file mode 100644 index 0000000..edbf642 --- /dev/null +++ b/src/lib/streak.ts @@ -0,0 +1,54 @@ +import { prisma } from './prisma'; + +/** + * Updates the user's writing streak based on their last write date. + * Should be called whenever a user performs a writing action (e.g., saving a chapter). + */ +export async function updateWritingStreak(userId: string) { + try { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { writingStreak: true, lastWriteDate: true } + }); + + if (!user) return; + + const now = new Date(); + const today = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())); + + let newStreak = user.writingStreak; + let lastWrite = user.lastWriteDate ? new Date(user.lastWriteDate) : null; + + if (lastWrite) { + const lastWriteDay = new Date(Date.UTC(lastWrite.getFullYear(), lastWrite.getMonth(), lastWrite.getDate())); + + // Calculate difference in days + const diffTime = today.getTime() - lastWriteDay.getTime(); + const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + // Already wrote today, do nothing but update the timestamp + } else if (diffDays === 1) { + // Wrote yesterday, increment streak + newStreak += 1; + } else { + // Missed a day (or more), reset streak to 1 + newStreak = 1; + } + } else { + // First time writing + newStreak = 1; + } + + // Update database + await prisma.user.update({ + where: { id: userId }, + data: { + writingStreak: newStreak, + lastWriteDate: now, + } + }); + } catch (error) { + console.error('Failed to update writing streak:', error); + } +} diff --git a/test-streak.ts b/test-streak.ts new file mode 100644 index 0000000..ec7a91e --- /dev/null +++ b/test-streak.ts @@ -0,0 +1,41 @@ +import { updateWritingStreak } from './src/lib/streak'; +import { prisma } from './src/lib/prisma'; + +async function main() { + // 1. Get the first user + const user = await prisma.user.findFirst(); + if (!user) { + console.log("No user found."); + return; + } + + console.log(`Original streak for ${user.email}:`, user.writingStreak, "Last write date:", user.lastWriteDate); + + // 2. Simulate setting lastWriteDate to yesterday + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + await prisma.user.update({ + where: { id: user.id }, + data: { + lastWriteDate: yesterday, + writingStreak: 5 // let's pretend it was 5 + } + }); + + console.log("Updated lastWriteDate to yesterday, streak is 5."); + + // 3. Call our function to update the streak (as if they wrote today) + console.log("Calling updateWritingStreak..."); + await updateWritingStreak(user.id); + + // 4. Verify the new values + const updatedUser = await prisma.user.findUnique({ + where: { id: user.id }, + select: { writingStreak: true, lastWriteDate: true } + }); + + console.log(`New streak:`, updatedUser?.writingStreak, "Last write date:", updatedUser?.lastWriteDate); +} + +main().catch(console.error);
{t('book_settings.subtitle')}