feat: Implement writing streak tracking, add chapter API routes, and introduce a BookSettings component with i18n support.
This commit is contained in:
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
Binary file not shown.
6
.next/dev/cache/turbopack/23c46498/LOG
vendored
6
.next/dev/cache/turbopack/23c46498/LOG
vendored
@@ -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)
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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)',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
54
src/lib/streak.ts
Normal file
54
src/lib/streak.ts
Normal 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
41
test-streak.ts
Normal 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);
|
||||
Reference in New Issue
Block a user