Compare commits

...

2 Commits

14 changed files with 235 additions and 55 deletions

Binary file not shown.

View File

@@ -6122,3 +6122,9 @@ FAM | META SEQ | SST SEQ | RANGE
0 | 00014933 | 00014932 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) 0 | 00014933 | 00014932 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00014934 | 00014930 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) 1 | 00014934 | 00014930 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00014935 | 00014931 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)

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -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); return NextResponse.json(updated);
} }

View File

@@ -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 }); return NextResponse.json(chapter, { status: 201 });
} }

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter, Merriweather } from "next/font/google"; import { Inter, Merriweather } from "next/font/google";
import Script from "next/script";
import { AuthProvider } from "@/providers/AuthProvider"; import { AuthProvider } from "@/providers/AuthProvider";
import { LanguageProvider } from "@/providers/LanguageProvider"; import { LanguageProvider } from "@/providers/LanguageProvider";
import "./globals.css"; import "./globals.css";
@@ -27,6 +28,14 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<head>
<Script
defer
src="http://kaelstudio-umami-42037c-158-220-111-183.traefik.me/script.js"
data-website-id="bce265f0-c9d4-4542-813f-f3bd9bf151bc"
strategy="afterInteractive"
/>
</head>
<body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}> <body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
<AuthProvider> <AuthProvider>
<LanguageProvider> <LanguageProvider>

View File

@@ -158,6 +158,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
<button <button
type="submit" type="submit"
data-umami-event={mode === 'signin' ? "Login" : mode === 'signup' ? "Signup" : "Send Reset"}
disabled={loading} disabled={loading}
className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4" className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4"
> >

View File

@@ -3,7 +3,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { BookProject, BookSettings } from '@/lib/types'; import { BookProject, BookSettings } from '@/lib/types';
import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants'; 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 { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations'; import { TranslationKey } from '@/lib/i18n/translations';
@@ -26,37 +26,73 @@ const DEFAULT_SETTINGS: BookSettings = {
const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => { const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => {
const { t } = useLanguage(); 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 [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showSavedFeedback, setShowSavedFeedback] = useState(false);
useEffect(() => { useEffect(() => {
setLocalTitle(project.title);
setLocalAuthor(project.author);
setLocalStyleGuide(project.styleGuide || '');
if (project.settings) { 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 handleChange = (key: keyof BookSettings, value: string) => {
const newSettings = { ...settings, [key]: value }; setLocalSettings(prev => ({ ...prev, [key]: value }));
setSettings(newSettings);
onUpdate({ ...project, settings: newSettings });
}; };
const handleStyleGuideChange = (value: string) => { const handleSave = () => {
onUpdate({ ...project, styleGuide: value }); 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 ( return (
<div className="h-full bg-theme-bg p-8 overflow-y-auto transition-colors duration-300"> <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-slate-900 text-white p-6 border-b border-slate-800 flex items-center justify-between">
<div className="bg-blue-600 p-3 rounded-lg"> <div className="flex items-center gap-4">
<Settings size={24} /> <div className="bg-blue-600 p-3 rounded-lg">
</div> <Settings size={24} />
<div> </div>
<h2 className="text-2xl font-bold">{t('book_settings.title')}</h2> <div>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p> <h2 className="text-2xl font-bold">{t('book_settings.title')}</h2>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p>
</div>
</div> </div>
<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>
<div className="p-8 space-y-8"> <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> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.novel_title')}</label>
<input <input
type="text" type="text"
value={project.title} value={localTitle}
onChange={(e) => onUpdate({ ...project, title: e.target.value })} 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" 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> </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> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.author_name')}</label>
<input <input
type="text" type="text"
value={project.author} value={localAuthor}
onChange={(e) => onUpdate({ ...project, author: e.target.value })} 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" 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> </div>
@@ -87,7 +123,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label>
<textarea <textarea
value={settings.synopsis} value={localSettings.synopsis}
onChange={(e) => handleChange('synopsis', e.target.value)} 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" 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')} placeholder={t('book_settings.synopsis_placeholder')}
@@ -105,7 +141,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<input <input
type="text" type="text"
list="genre-suggestions" list="genre-suggestions"
value={settings.genre} value={localSettings.genre}
onChange={(e) => handleChange('genre', e.target.value)} 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" 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')} 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> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.sub_genre')}</label>
<input <input
type="text" type="text"
value={settings.subGenre || ''} value={localSettings.subGenre || ''}
onChange={(e) => handleChange('subGenre', e.target.value)} 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" 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')} 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> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.target_audience')}</label>
<input <input
type="text" type="text"
value={settings.targetAudience} value={localSettings.targetAudience}
onChange={(e) => handleChange('targetAudience', e.target.value)} 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" 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')} 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" /> <Hash size={14} className="absolute left-3 top-3 text-theme-muted" />
<input <input
type="text" type="text"
value={settings.themes} value={localSettings.themes}
onChange={(e) => handleChange('themes', e.target.value)} 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" 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')} placeholder={t('book_settings.themes_placeholder')}
@@ -160,7 +196,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Users size={14} /> {t('book_settings.pov')} <Users size={14} /> {t('book_settings.pov')}
</label> </label>
<select <select
value={settings.pov} value={localSettings.pov}
onChange={(e) => handleChange('pov', e.target.value)} 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" 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')} <Clock size={14} /> {t('book_settings.tense')}
</label> </label>
<select <select
value={settings.tense} value={localSettings.tense}
onChange={(e) => handleChange('tense', e.target.value)} 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" 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 <input
type="text" type="text"
list="tone-suggestions" list="tone-suggestions"
value={settings.tone} value={localSettings.tone}
onChange={(e) => handleChange('tone', e.target.value)} 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" 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')} placeholder={t('book_settings.tone_placeholder')}
@@ -205,8 +241,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
{t('book_settings.style_guide_help')} {t('book_settings.style_guide_help')}
</p> </p>
<textarea <textarea
value={project.styleGuide || ''} value={localStyleGuide}
onChange={(e) => handleStyleGuideChange(e.target.value)} 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" 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')} placeholder={t('book_settings.style_guide_placeholder')}
/> />

View File

@@ -47,10 +47,10 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<LanguageSwitcher /> <LanguageSwitcher />
<button onClick={onProfile} className="bg-theme-bg text-theme-text px-4 md:px-5 py-2 md:py-2.5 rounded-xl text-xs md:text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border"> <button onClick={onProfile} data-umami-event="Go To Profile" className="bg-theme-bg text-theme-text px-4 md:px-5 py-2 md:py-2.5 rounded-xl text-xs md:text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border">
<User size={18} /> {t('dashboard.my_profile')} <User size={18} /> {t('dashboard.my_profile')}
</button> </button>
<button onClick={onLogout} title={t('sidebar.logout')} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button> <button onClick={onLogout} data-umami-event="Logout" title={t('sidebar.logout')} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button>
</div> </div>
</div> </div>
@@ -86,6 +86,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<h3 className="text-2xl font-black text-theme-text">{t('dashboard.my_novels')}</h3> <h3 className="text-2xl font-black text-theme-text">{t('dashboard.my_novels')}</h3>
<button <button
onClick={onCreate} onClick={onCreate}
data-umami-event="Create Project"
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200" className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200"
> >
<Plus size={20} /> {t('dashboard.write_new')} <Plus size={20} /> {t('dashboard.write_new')}
@@ -97,6 +98,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div <div
key={p.id} key={p.id}
onClick={() => onSelect(p.id)} onClick={() => onSelect(p.id)}
data-umami-event="Open Project"
className="bg-theme-panel p-6 md:p-8 rounded-[2.5rem] border border-theme-border shadow-sm hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between h-64" className="bg-theme-panel p-6 md:p-8 rounded-[2.5rem] border border-theme-border shadow-sm hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between h-64"
> >
<div> <div>
@@ -153,7 +155,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div> </div>
</div> </div>
</div> </div>
<button onClick={onPricing} className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all"> <button onClick={onPricing} data-umami-event="Pricing Click" className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all">
{t('dashboard.upgrade_plan')} {t('dashboard.upgrade_plan')}
</button> </button>
</div> </div>

View File

@@ -47,6 +47,7 @@ export const LanguageSwitcher: React.FC = () => {
{languages.map((lang) => ( {languages.map((lang) => (
<button <button
key={lang.code} key={lang.code}
data-umami-event="Change Language"
onClick={() => { onClick={() => {
setLanguage(lang.code); setLanguage(lang.code);
setIsOpen(false); setIsOpen(false);

View File

@@ -582,6 +582,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('correct')} onClick={() => handleAiAction('correct')}
data-umami-event="AI Correct"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`}
> >
@@ -590,6 +591,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('rewrite')} onClick={() => handleAiAction('rewrite')}
data-umami-event="AI Rewrite"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`}
> >
@@ -598,6 +600,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('expand')} onClick={() => handleAiAction('expand')}
data-umami-event="AI Expand"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`}
> >
@@ -606,6 +609,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('continue')} onClick={() => handleAiAction('continue')}
data-umami-event="AI Continue"
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-700 text-left transition-colors" className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-700 text-left transition-colors"
> >
<Wand2 size={14} /> Continuer l'écriture <Wand2 size={14} /> Continuer l'écriture
@@ -619,6 +623,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={handleCopy} onClick={handleCopy}
data-umami-event="Editor Copy"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-slate-50'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-slate-50'}`}
> >
@@ -627,6 +632,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={handleSelectAll} onClick={handleSelectAll}
data-umami-event="Editor Select All"
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors" className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors"
> >
<MousePointerClick size={14} /> Tout sélectionner <MousePointerClick size={14} /> Tout sélectionner

View File

@@ -94,6 +94,8 @@ export const translations = {
'book_settings.confirm_delete': 'Oui, supprimer définitivement', 'book_settings.confirm_delete': 'Oui, supprimer définitivement',
'book_settings.cancel': 'Annuler', 'book_settings.cancel': 'Annuler',
'book_settings.delete_button': 'Supprimer ce projet', 'book_settings.delete_button': 'Supprimer ce projet',
'book_settings.save': 'Sauvegarder',
'book_settings.saved': 'Sauvegardé',
// Landing Page // Landing Page
'landing.nav_features': 'Fonctionnalités', 'landing.nav_features': 'Fonctionnalités',
@@ -323,11 +325,12 @@ export const translations = {
'sw.save_color': '+ SAUVER', 'sw.save_color': '+ SAUVER',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'Première Personne', 'pov_options.1ère_personne_(je)': '1ère personne (Je)',
'pov_options.troisième_personne_limitée': 'Troisième Personne Limitée', 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3ème personne (Limitée au protagoniste)',
'pov_options.troisième_personne_omnisciente': 'Troisième Personne Omnisciente', 'pov_options.3ème_personne_(omnisciente)': '3ème personne (Omnisciente)',
'tense_options.présent': 'Présent', 'pov_options.multi-points_de_vue_(alterné)': 'Multi-points de vue (Alterné)',
'tense_options.passé': 'Passé', 'tense_options.passé_(passé_simple_/_imparfait)': 'Passé (Passé simple / Imparfait)',
'tense_options.présent_de_narration': 'Présent de narration',
}, },
en: { en: {
// General Navigation // General Navigation
@@ -422,6 +425,8 @@ export const translations = {
'book_settings.confirm_delete': 'Yes, delete permanently', 'book_settings.confirm_delete': 'Yes, delete permanently',
'book_settings.cancel': 'Cancel', 'book_settings.cancel': 'Cancel',
'book_settings.delete_button': 'Delete this project', 'book_settings.delete_button': 'Delete this project',
'book_settings.save': 'Save',
'book_settings.saved': 'Saved',
// Landing Page // Landing Page
'landing.nav_features': 'Features', 'landing.nav_features': 'Features',
@@ -651,11 +656,12 @@ export const translations = {
'sw.save_color': '+ SAVE', 'sw.save_color': '+ SAVE',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'First Person', 'pov_options.1ère_personne_(je)': '1st Person (I)',
'pov_options.troisième_personne_limitée': 'Third Person Limited', 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3rd Person (Limited)',
'pov_options.troisième_personne_omnisciente': 'Third Person Omniscient', 'pov_options.3ème_personne_(omnisciente)': '3rd Person (Omniscient)',
'tense_options.présent': 'Present', 'pov_options.multi-points_de_vue_(alterné)': 'Multi-POV (Alternating)',
'tense_options.passé': 'Past', 'tense_options.passé_(passé_simple_/_imparfait)': 'Past Tense',
'tense_options.présent_de_narration': 'Present Tense',
}, },
es: { es: {
// General Navigation // General Navigation
@@ -750,6 +756,8 @@ export const translations = {
'book_settings.confirm_delete': 'Sí, eliminar permanentemente', 'book_settings.confirm_delete': 'Sí, eliminar permanentemente',
'book_settings.cancel': 'Cancelar', 'book_settings.cancel': 'Cancelar',
'book_settings.delete_button': 'Eliminar este proyecto', 'book_settings.delete_button': 'Eliminar este proyecto',
'book_settings.save': 'Guardar',
'book_settings.saved': 'Guardado',
// Landing Page // Landing Page
'landing.nav_features': 'Características', 'landing.nav_features': 'Características',
@@ -979,11 +987,12 @@ export const translations = {
'sw.save_color': '+ GUARDAR', 'sw.save_color': '+ GUARDAR',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'Primera Persona', 'pov_options.1ère_personne_(je)': ' Persona (Yo)',
'pov_options.troisième_personne_limitée': 'Tercera Persona Limitada', 'pov_options.3ème_personne_(limitée_au_protagoniste)': ' Persona (Limitada)',
'pov_options.troisième_personne_omnisciente': 'Tercera Persona Omnisciente', 'pov_options.3ème_personne_(omnisciente)': ' Persona (Omnisciente)',
'tense_options.présent': 'Presente', 'pov_options.multi-points_de_vue_(alterné)': 'Múltiples POV (Alternando)',
'tense_options.passé': 'Pasado', 'tense_options.passé_(passé_simple_/_imparfait)': 'Tiempo Pasado',
'tense_options.présent_de_narration': 'Tiempo Presente',
}, },
de: { de: {
// General Navigation // General Navigation
@@ -1078,6 +1087,8 @@ export const translations = {
'book_settings.confirm_delete': 'Ja, dauerhaft löschen', 'book_settings.confirm_delete': 'Ja, dauerhaft löschen',
'book_settings.cancel': 'Abbrechen', 'book_settings.cancel': 'Abbrechen',
'book_settings.delete_button': 'Dieses Projekt löschen', 'book_settings.delete_button': 'Dieses Projekt löschen',
'book_settings.save': 'Speichern',
'book_settings.saved': 'Gespeichert',
// Landing Page // Landing Page
'landing.nav_features': 'Funktionen', 'landing.nav_features': 'Funktionen',
@@ -1307,11 +1318,12 @@ export const translations = {
'sw.save_color': '+ SPEICHERN', 'sw.save_color': '+ SPEICHERN',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'Ich-Perspektive', 'pov_options.1ère_personne_(je)': 'Ich-Perspektive',
'pov_options.troisième_personne_limitée': 'Personale Erzählsituation', 'pov_options.3ème_personne_(limitée_au_protagoniste)': 'Er/Sie-Perspektive (personal)',
'pov_options.troisième_personne_omnisciente': 'Auktorialer Erzähler', 'pov_options.3ème_personne_(omnisciente)': 'Er/Sie-Perspektive (auktorial)',
'tense_options.présent': 'Präsens', 'pov_options.multi-points_de_vue_(alterné)': 'Mehrere Perspektiven',
'tense_options.passé': 'Präteritum', 'tense_options.passé_(passé_simple_/_imparfait)': 'Vergangenheit (Präteritum)',
'tense_options.présent_de_narration': 'Gegenwart (Präsens)',
} }
}; };

54
src/lib/streak.ts Normal file
View File

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

41
test-streak.ts Normal file
View File

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