Learn how to add a personalized journal with mood tracking to your web app for better user engagement and insights.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Journaling and Mood Tracking Matter for Your Users
Adding a journaling feature with mood tracking isn't just a nice-to-have—it's becoming an expectation in apps focused on personal growth, wellness, or productivity. Users increasingly look for digital tools that help them understand themselves better, and capturing both thoughts and emotional states creates a powerful feedback loop for self-awareness.
The Three Pillars of a Journal + Mood System
Let me walk you through implementing this feature end-to-end, with code examples for each component.
Database Schema Design
You'll need at least two main tables: one for journal entries and one for mood tracking. Here's a PostgreSQL example:
-- Journal entries table
CREATE TABLE journal_entries (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
entry_date DATE NOT NULL DEFAULT CURRENT_DATE,
content TEXT,
title VARCHAR(255),
tags TEXT[], -- Using array type for flexible tagging
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(user_id, entry_date) -- Optional: limit to one entry per day
);
-- Mood tracking table
CREATE TABLE mood_entries (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
journal_entry_id INTEGER REFERENCES journal_entries(id),
recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
mood_score INTEGER NOT NULL, -- e.g., 1-10 scale
energy_level INTEGER, -- Optional: track energy separately
mood_notes TEXT, -- Allow qualitative description
associated_activities TEXT[] -- What was user doing?
);
-- Index for performance
CREATE INDEX idx_journal_user_date ON journal_entries(user_id, entry_date);
CREATE INDEX idx_mood_user_date ON mood_entries(user_id, recorded_at);
For NoSQL alternatives like MongoDB, you might structure your documents like this:
// Journal entry document structure
{
_id: ObjectId("..."),
userId: ObjectId("..."),
entryDate: ISODate("2023-09-25"),
content: "Today I worked on the new feature...",
title: "Making progress",
tags: ["work", "coding", "achievement"],
moods: [
{
recordedAt: ISODate("2023-09-25T10:30:00Z"),
score: 7,
energyLevel: 8,
notes: "Feeling productive after coffee",
activities: ["coding", "meeting"]
},
{
recordedAt: ISODate("2023-09-25T16:45:00Z"),
score: 5,
energyLevel: 4,
notes: "Energy dipping after long meeting",
activities: ["meeting"]
}
],
createdAt: ISODate("2023-09-25T09:15:00Z"),
updatedAt: ISODate("2023-09-25T16:45:00Z")
}
Core Endpoints You'll Need
Here's a Node.js/Express implementation of the key endpoints:
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
const JournalEntry = require('../models/JournalEntry');
const MoodEntry = require('../models/MoodEntry');
// Create/update a journal entry
router.post('/journal', authenticate, async (req, res) => {
try {
const { entryDate, content, title, tags } = req.body;
const userId = req.user.id;
// Using upsert to handle both creation and updates
const entry = await JournalEntry.findOneAndUpdate(
{ userId, entryDate },
{ content, title, tags, updatedAt: new Date() },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
res.status(200).json(entry);
} catch (error) {
console.error('Error saving journal:', error);
res.status(500).json({ message: 'Failed to save journal entry' });
}
});
// Record a mood entry
router.post('/mood', authenticate, async (req, res) => {
try {
const {
journalEntryId,
moodScore,
energyLevel,
moodNotes,
associatedActivities
} = req.body;
const userId = req.user.id;
const moodEntry = new MoodEntry({
userId,
journalEntryId,
moodScore,
energyLevel,
moodNotes,
associatedActivities,
recordedAt: new Date()
});
await moodEntry.save();
// If this mood is attached to a journal entry, update it
if (journalEntryId) {
await JournalEntry.findByIdAndUpdate(
journalEntryId,
{ updatedAt: new Date() }
);
}
res.status(201).json(moodEntry);
} catch (error) {
console.error('Error recording mood:', error);
res.status(500).json({ message: 'Failed to record mood' });
}
});
// Get journal entries with pagination
router.get('/journal', authenticate, async (req, res) => {
try {
const userId = req.user.id;
const { page = 1, limit = 10, startDate, endDate } = req.query;
const query = { userId };
// Add date filtering if provided
if (startDate || endDate) {
query.entryDate = {};
if (startDate) query.entryDate.$gte = new Date(startDate);
if (endDate) query.entryDate.$lte = new Date(endDate);
}
const entries = await JournalEntry.find(query)
.sort({ entryDate: -1 })
.limit(parseInt(limit))
.skip((parseInt(page) - 1) * parseInt(limit));
const total = await JournalEntry.countDocuments(query);
res.status(200).json({
entries,
pagination: {
total,
page: parseInt(page),
pages: Math.ceil(total / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching journal entries:', error);
res.status(500).json({ message: 'Failed to fetch journal entries' });
}
});
// Get mood analytics
router.get('/mood/analytics', authenticate, async (req, res) => {
try {
const userId = req.user.id;
const { period = 'month' } = req.query; // 'week', 'month', 'year'
// Calculate start date based on period
const startDate = new Date();
if (period === 'week') startDate.setDate(startDate.getDate() - 7);
else if (period === 'month') startDate.setMonth(startDate.getMonth() - 1);
else if (period === 'year') startDate.setFullYear(startDate.getFullYear() - 1);
const moodData = await MoodEntry.aggregate([
{
$match: {
userId: userId,
recordedAt: { $gte: startDate }
}
},
{
$group: {
_id: {
// Group by day for proper visualization
year: { $year: "$recordedAt" },
month: { $month: "$recordedAt" },
day: { $dayOfMonth: "$recordedAt" }
},
avgMood: { $avg: "$moodScore" },
avgEnergy: { $avg: "$energyLevel" },
count: { $sum: 1 }
}
},
{ $sort: { "_id.year": 1, "_id.month": 1, "_id.day": 1 } }
]);
// Format for frontend charting
const formattedData = moodData.map(item => {
const date = new Date(
item._id.year,
item._id.month - 1,
item._id.day
);
return {
date: date.toISOString().split('T')[0],
avgMood: parseFloat(item.avgMood.toFixed(1)),
avgEnergy: item.avgEnergy ? parseFloat(item.avgEnergy.toFixed(1)) : null,
count: item.count
};
});
res.status(200).json(formattedData);
} catch (error) {
console.error('Error fetching mood analytics:', error);
res.status(500).json({ message: 'Failed to fetch mood analytics' });
}
});
module.exports = router;
Journal Entry Component
Here's a React component for the journal entry interface:
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Editor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { format } from 'date-fns';
import MoodTracker from './MoodTracker';
import './JournalEntry.css';
const JournalEntry = () => {
const { date } = useParams(); // Format: YYYY-MM-DD
const navigate = useNavigate();
const [entry, setEntry] = useState({
title: '',
content: '',
tags: [],
});
const [saving, setSaving] = useState(false);
const [lastSaved, setLastSaved] = useState(null);
const journalDate = date ? new Date(date) : new Date();
const formattedDate = format(journalDate, 'EEEE, MMMM d, yyyy');
// Setup rich text editor
const editor = useEditor({
extensions: [StarterKit],
content: entry.content,
onUpdate: ({ editor }) => {
setEntry(prev => ({
...prev,
content: editor.getHTML()
}));
// Trigger auto-save
debouncedSave();
}
});
// Load existing entry if available
useEffect(() => {
const fetchEntry = async () => {
try {
const response = await fetch(`/api/journal?entryDate=${date}`);
if (response.ok) {
const data = await response.json();
if (data.entries.length > 0) {
const loadedEntry = data.entries[0];
setEntry({
title: loadedEntry.title || '',
content: loadedEntry.content || '',
tags: loadedEntry.tags || []
});
editor?.commands.setContent(loadedEntry.content || '');
setLastSaved(new Date(loadedEntry.updatedAt));
}
}
} catch (error) {
console.error('Error loading journal entry:', error);
}
};
if (date) {
fetchEntry();
}
}, [date, editor]);
// Save the journal entry
const saveEntry = async () => {
if (!entry.content && !entry.title) return;
setSaving(true);
try {
const response = await fetch('/api/journal', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
entryDate: date || format(new Date(), 'yyyy-MM-dd'),
title: entry.title,
content: entry.content,
tags: entry.tags
})
});
if (response.ok) {
const savedEntry = await response.json();
setLastSaved(new Date(savedEntry.updatedAt));
}
} catch (error) {
console.error('Error saving journal entry:', error);
// Show error toast/notification
} finally {
setSaving(false);
}
};
// Debounce save function to avoid excessive API calls
const debouncedSave = useCallback(
debounce(saveEntry, 2000),
[entry]
);
return (
<div className="journal-entry-container">
<div className="journal-header">
<h3>{formattedDate}</h3>
<div className="journal-actions">
{lastSaved && (
<span className="last-saved">
Last saved: {format(lastSaved, 'h:mm a')}
</span>
)}
<button
className="save-button"
onClick={saveEntry}
disabled={saving}
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
<input
type="text"
className="journal-title"
placeholder="Title your entry..."
value={entry.title}
onChange={(e) => setEntry(prev => ({ ...prev, title: e.target.value }))}
onBlur={debouncedSave}
/>
<div className="editor-container">
{editor && (
<EditorContent
editor={editor}
className="journal-content"
/>
)}
</div>
<div className="tag-container">
{entry.tags.map(tag => (
<span key={tag} className="tag">
{tag}
<button
className="remove-tag"
onClick={() => {
setEntry(prev => ({
...prev,
tags: prev.tags.filter(t => t !== tag)
}));
debouncedSave();
}}
>
×
</button>
</span>
))}
<input
type="text"
className="tag-input"
placeholder="Add a tag..."
onKeyDown={(e) => {
if (e.key === 'Enter' && e.target.value.trim()) {
setEntry(prev => ({
...prev,
tags: [...new Set([...prev.tags, e.target.value.trim()])]
}));
e.target.value = '';
debouncedSave();
}
}}
/>
</div>
<MoodTracker entryDate={date} />
</div>
);
};
// Utility function for debouncing
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
export default JournalEntry;
Mood Tracking Component
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';
import './MoodTracker.css';
const MOOD_EMOJIS = {
1: '😢', // Very sad
2: '😔', // Sad
3: '😐', // Neutral
4: '🙂', // Happy
5: '😄' // Very happy
};
const ENERGY_LEVELS = {
1: 'Very Low',
2: 'Low',
3: 'Moderate',
4: 'High',
5: 'Very High'
};
const COMMON_ACTIVITIES = [
'Work', 'Exercise', 'Family', 'Friends',
'Reading', 'TV/Movies', 'Meditation', 'Outdoors',
'Cooking', 'Shopping', 'Cleaning', 'Travel'
];
const MoodTracker = ({ entryDate }) => {
const [moods, setMoods] = useState([]);
const [showMoodForm, setShowMoodForm] = useState(false);
const [newMood, setNewMood] = useState({
moodScore: 3,
energyLevel: 3,
moodNotes: '',
associatedActivities: []
});
// Load existing moods for this entry date
useEffect(() => {
const fetchMoods = async () => {
try {
const formattedDate = entryDate || format(new Date(), 'yyyy-MM-dd');
const response = await fetch(`/api/mood?date=${formattedDate}`);
if (response.ok) {
const data = await response.json();
setMoods(data.moods || []);
}
} catch (error) {
console.error('Error fetching moods:', error);
}
};
fetchMoods();
}, [entryDate]);
const handleSubmitMood = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/mood', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...newMood,
entryDate: entryDate || format(new Date(), 'yyyy-MM-dd')
})
});
if (response.ok) {
const savedMood = await response.json();
setMoods(prev => [...prev, savedMood]);
setShowMoodForm(false);
setNewMood({
moodScore: 3,
energyLevel: 3,
moodNotes: '',
associatedActivities: []
});
}
} catch (error) {
console.error('Error saving mood:', error);
}
};
const toggleActivity = (activity) => {
setNewMood(prev => {
const activities = prev.associatedActivities.includes(activity)
? prev.associatedActivities.filter(a => a !== activity)
: [...prev.associatedActivities, activity];
return {
...prev,
associatedActivities: activities
};
});
};
return (
<div className="mood-tracker-container">
<div className="mood-header">
<h4>Mood Tracker</h4>
{!showMoodForm && (
<button
className="add-mood-button"
onClick={() => setShowMoodForm(true)}
>
+ Add Mood
</button>
)}
</div>
{showMoodForm && (
<form className="mood-form" onSubmit={handleSubmitMood}>
<div className="mood-selector">
<label>How are you feeling?</label>
<div className="emoji-slider">
{Object.entries(MOOD_EMOJIS).map(([value, emoji]) => (
<button
key={value}
type="button"
className={`emoji-option ${newMood.moodScore === Number(value) ? 'selected' : ''}`}
onClick={() => setNewMood(prev => ({ ...prev, moodScore: Number(value) }))}
>
{emoji}
</button>
))}
</div>
</div>
<div className="energy-selector">
<label>Energy Level</label>
<div className="slider-container">
<input
type="range"
min="1"
max="5"
value={newMood.energyLevel}
onChange={(e) => setNewMood(prev => ({
...prev,
energyLevel: Number(e.target.value)
}))}
/>
<span className="energy-label">
{ENERGY_LEVELS[newMood.energyLevel]}
</span>
</div>
</div>
<div className="activities-selector">
<label>What are you doing?</label>
<div className="activity-tags">
{COMMON_ACTIVITIES.map(activity => (
<button
key={activity}
type="button"
className={`activity-tag ${
newMood.associatedActivities.includes(activity)
? 'selected'
: ''
}`}
onClick={() => toggleActivity(activity)}
>
{activity}
</button>
))}
</div>
</div>
<div className="mood-notes">
<label>Notes</label>
<textarea
placeholder="What's contributing to your mood?"
value={newMood.moodNotes}
onChange={(e) => setNewMood(prev => ({
...prev,
moodNotes: e.target.value
}))}
/>
</div>
<div className="form-actions">
<button
type="button"
className="cancel-button"
onClick={() => setShowMoodForm(false)}
>
Cancel
</button>
<button type="submit" className="save-button">
Save Mood
</button>
</div>
</form>
)}
{moods.length > 0 && (
<div className="mood-history">
<h5>Today's Moods</h5>
<div className="mood-entries">
{moods.map(mood => (
<div key={mood.id} className="mood-entry">
<div className="mood-time">
{format(new Date(mood.recordedAt), 'h:mm a')}
</div>
<div className="mood-emoji">
{MOOD_EMOJIS[mood.moodScore]}
</div>
<div className="mood-details">
<div className="mood-energy">
Energy: {ENERGY_LEVELS[mood.energyLevel]}
</div>
{mood.associatedActivities.length > 0 && (
<div className="mood-activities">
{mood.associatedActivities.join(', ')}
</div>
)}
{mood.moodNotes && (
<div className="mood-note">
"{mood.moodNotes}"
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default MoodTracker;
Visualization Component
import React, { useState, useEffect } from 'react';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
} from 'chart.js';
import { format, subDays, subMonths } from 'date-fns';
import './MoodAnalytics.css';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const MoodAnalytics = () => {
const [timeframe, setTimeframe] = useState('month');
const [moodData, setMoodData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [commonActivities, setCommonActivities] = useState([]);
const [insightText, setInsightText] = useState('');
useEffect(() => {
const fetchMoodData = async () => {
setLoading(true);
try {
const response = await fetch(`/api/mood/analytics?period=${timeframe}`);
if (response.ok) {
const data = await response.json();
setMoodData(data);
// Generate insights from the data
generateInsights(data);
// Find most common activities associated with moods
analyzeActivities(data);
} else {
throw new Error('Failed to fetch mood data');
}
} catch (error) {
console.error('Error fetching mood analytics:', error);
setError('Unable to load your mood data. Please try again later.');
} finally {
setLoading(false);
}
};
fetchMoodData();
}, [timeframe]);
const generateInsights = (data) => {
if (data.length === 0) {
setInsightText('Start tracking your mood to see insights here.');
return;
}
// Calculate average mood
const avgMood = data.reduce((sum, day) => sum + day.avgMood, 0) / data.length;
// Find highest and lowest days
const highestDay = [...data].sort((a, b) => b.avgMood - a.avgMood)[0];
const lowestDay = [...data].sort((a, b) => a.avgMood - b.avgMood)[0];
// Look for trends
let trend = 'steady';
if (data.length >= 7) {
const firstWeek = data.slice(0, 7).reduce((sum, day) => sum + day.avgMood, 0) / 7;
const lastWeek = data.slice(-7).reduce((sum, day) => sum + day.avgMood, 0) / 7;
if (lastWeek - firstWeek > 0.5) trend = 'improving';
else if (firstWeek - lastWeek > 0.5) trend = 'declining';
}
// Generate insight text
let insight = `Your average mood has been ${avgMood.toFixed(1)}/5, which is `;
insight += avgMood >= 4 ? 'quite positive! ' : avgMood >= 3 ? 'moderate. ' : 'on the lower side. ';
if (highestDay) {
insight += `Your best day was ${format(new Date(highestDay.date), 'MMMM d')} `;
insight += `with an average mood of ${highestDay.avgMood.toFixed(1)}/5. `;
}
if (trend === 'improving') {
insight += 'Your mood has been trending upward lately - whatever you\'re doing, keep it up!';
} else if (trend === 'declining') {
insight += 'Your mood has been trending downward recently. It might be worth reflecting on what\'s changed.';
} else {
insight += 'Your mood has been relatively consistent during this period.';
}
setInsightText(insight);
};
const analyzeActivities = (data) => {
// This would be more accurate with the raw data rather than daily averages
// For this example, we'll assume we have activity data in our analytics response
// Sample implementation:
const activityCounts = {};
const activityMoods = {};
data.forEach(day => {
if (day.activities) {
day.activities.forEach(activity => {
activityCounts[activity] = (activityCounts[activity] || 0) + 1;
activityMoods[activity] = activityMoods[activity] || [];
activityMoods[activity].push(day.avgMood);
});
}
});
// Calculate average mood for each activity
const activityAvgMoods = Object.keys(activityMoods).map(activity => {
const moods = activityMoods[activity];
const avgMood = moods.reduce((sum, mood) => sum + mood, 0) / moods.length;
return {
activity,
count: activityCounts[activity],
avgMood
};
});
// Sort by frequency
activityAvgMoods.sort((a, b) => b.count - a.count);
setCommonActivities(activityAvgMoods.slice(0, 5)); // Top 5 activities
};
const chartData = {
labels: moodData.map(day => format(new Date(day.date), 'MMM d')),
datasets: [
{
label: 'Mood',
data: moodData.map(day => day.avgMood),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4
},
{
label: 'Energy',
data: moodData.map(day => day.avgEnergy),
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.2)',
tension: 0.4
}
]
};
const chartOptions = {
responsive: true,
scales: {
y: {
min: 1,
max: 5,
ticks: {
stepSize: 1
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const dayData = moodData[context.dataIndex];
let result = `${label}: ${value.toFixed(1)}`;
if (dayData.count) {
result += ` (from ${dayData.count} entries)`;
}
return result;
}
}
}
}
};
return (
<div className="mood-analytics-container">
<div className="analytics-header">
<h3>Mood Insights</h3>
<div className="timeframe-selector">
<button
className={timeframe === 'week' ? 'active' : ''}
onClick={() => setTimeframe('week')}
>
Week
</button>
<button
className={timeframe === 'month' ? 'active' : ''}
onClick={() => setTimeframe('month')}
>
Month
</button>
<button
className={timeframe === 'year' ? 'active' : ''}
onClick={() => setTimeframe('year')}
>
Year
</button>
</div>
</div>
{loading ? (
<div className="loading-indicator">Loading your mood data...</div>
) : error ? (
<div className="error-message">{error}</div>
) : moodData.length === 0 ? (
<div className="empty-state">
<p>You haven't tracked any moods in this time period yet.</p>
<p>Start recording your moods in your daily journal to see trends here.</p>
</div>
) : (
<>
<div className="chart-container">
<Line data={chartData} options={chartOptions} />
</div>
<div className="insights-container">
<div className="insight-card">
<h4>Mood Insights</h4>
<p>{insightText}</p>
</div>
{commonActivities.length > 0 && (
<div className="activity-insights">
<h4>Activities & Your Mood</h4>
<div className="activity-list">
{commonActivities.map(item => (
<div key={item.activity} className="activity-item">
<div className="activity-name">{item.activity}</div>
<div className="activity-bar-container">
<div
className="activity-bar"
style={{
width: `${(item.avgMood / 5) * 100}%`,
backgroundColor: item.avgMood >= 4
? '#4CAF50'
: item.avgMood >= 3
? '#FFC107'
: '#F44336'
}}
/>
</div>
<div className="activity-mood">{item.avgMood.toFixed(1)}</div>
</div>
))}
</div>
<p className="activity-insight-text">
{commonActivities[0]?.avgMood > 4
? `${commonActivities[0].activity} seems to boost your mood the most!`
: commonActivities[0]?.avgMood < 3
? `${commonActivities[0].activity} tends to coincide with lower moods.`
: 'Your activities have a varying impact on your mood.'}
</p>
</div>
)}
</div>
</>
)}
</div>
);
};
export default MoodAnalytics;
Integration with Your Main App
Here's how to integrate these features into your main app structure:
// App.js or your main router file
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Dashboard from './pages/Dashboard';
import JournalEntry from './pages/JournalEntry';
import JournalList from './pages/JournalList';
import MoodAnalytics from './pages/MoodAnalytics';
import Settings from './pages/Settings';
import PrivateRoute from './components/PrivateRoute';
function App() {
return (
<BrowserRouter>
<div className="app-container">
<Navbar />
<div className="main-content">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/journal" element={
<PrivateRoute>
<JournalList />
</PrivateRoute>
} />
<Route path="/journal/:date" element={
<PrivateRoute>
<JournalEntry />
</PrivateRoute>
} />
<Route path="/journal/new" element={
<PrivateRoute>
<JournalEntry />
</PrivateRoute>
} />
<Route path="/mood-insights" element={
<PrivateRoute>
<MoodAnalytics />
</PrivateRoute>
} />
<Route path="/settings" element={
<PrivateRoute>
<Settings />
</PrivateRoute>
} />
</Routes>
</div>
</div>
</BrowserRouter>
);
}
export default App;
Enhancing Your Journal with Smart Features
Here's a quick example of implementing sentiment analysis with a library like sentiment:
const Sentiment = require('sentiment');
const sentiment = new Sentiment();
// In your journal entry save endpoint
router.post('/journal', authenticate, async (req, res) => {
try {
const { entryDate, content, title, tags } = req.body;
const userId = req.user.id;
// Analyze sentiment of the journal content
let suggestedMood = null;
if (content) {
const result = sentiment.analyze(content);
// Convert sentiment score to a 1-5 scale
// Sentiment typically ranges from around -5 to +5
const normalizedScore = Math.min(Math.max((result.score + 5) / 2, 1), 5);
suggestedMood = Math.round(normalizedScore);
}
// Save the entry as before
const entry = await JournalEntry.findOneAndUpdate(
{ userId, entryDate },
{ content, title, tags, updatedAt: new Date() },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
// If we have a suggested mood and the user hasn't recorded a mood yet today
if (suggestedMood) {
const existingMood = await MoodEntry.findOne({
userId,
recordedAt: {
$gte: new Date(entryDate),
$lt: new Date(entryDate + 'T23:59:59')
}
});
if (!existingMood) {
res.status(200).json({
entry,
suggestedMood,
message: "Based on your journal entry, would you like to record your mood as " +
`${suggestedMood}/5?`
});
return;
}
}
res.status(200).json({ entry });
} catch (error) {
console.error('Error saving journal:', error);
res.status(500).json({ message: 'Failed to save journal entry' });
}
});
Keeping Data Safe and Fast
Here's a simple implementation of caching for analytics results:
const NodeCache = require('node-cache');
const analyticsCache = new NodeCache({ stdTTL: 3600 }); // Cache for 1 hour
// In your analytics endpoint
router.get('/mood/analytics', authenticate, async (req, res) => {
try {
const userId = req.user.id;
const { period = 'month' } = req.query;
// Create a cache key based on user ID and period
const cacheKey = `mood-analytics-${userId}-${period}`;
// Check if we have cached results
const cachedData = analyticsCache.get(cacheKey);
if (cachedData) {
return res.status(200).json(cachedData);
}
// If not cached, calculate analytics as before
const startDate = new Date();
if (period === 'week') startDate.setDate(startDate.getDate() - 7);
else if (period === 'month') startDate.setMonth(startDate.getMonth() - 1);
else if (period === 'year') startDate.setFullYear(startDate.getFullYear() - 1);
const moodData = await MoodEntry.aggregate([
/* ...aggregation pipeline as before... */
]);
const formattedData = moodData.map(item => {
/* ...format data as before... */
});
// Cache the results before sending
analyticsCache.set(cacheKey, formattedData);
res.status(200).json(formattedData);
} catch (error) {
console.error('Error fetching mood analytics:', error);
res.status(500).json({ message: 'Failed to fetch mood analytics' });
}
});
// Invalidate cache when new moods are added
router.post('/mood', authenticate, async (req, res) => {
try {
// Process and save mood as before
// Invalidate any cached analytics for this user
const cacheKeys = [
`mood-analytics-${req.user.id}-week`,
`mood-analytics-${req.user.id}-month`,
`mood-analytics-${req.user.id}-year`
];
cacheKeys.forEach(key => analyticsCache.del(key));
res.status(201).json(moodEntry);
} catch (error) {
console.error('Error recording mood:', error);
res.status(500).json({ message: 'Failed to record mood' });
}
});
Adding a personalized journal with mood tracking to your web app involves multiple connected components, from database design to UI implementation. The code examples provided here give you a solid foundation to build upon, but remember:
With the right implementation, a journaling feature can significantly increase user engagement and retention. Users who track their moods and thoughts over time develop a relationship with your app that goes beyond the transactional—they're literally recording pieces of their lives within your product.
Explore the top 3 personalized journal use cases with mood tracking to enhance your web app experience.
A comprehensive solution for mental health professionals and their patients to track mood patterns alongside treatment efficacy. The data correlation between journal entries and mood scores creates actionable insights that can inform therapeutic approaches and medication adjustments in real-time, rather than relying solely on session recall.
A reflective tool for individuals on self-improvement journeys seeking to understand their emotional triggers and response patterns. By connecting written thoughts with emotional states, users gain a metacognitive advantage - seeing not just what happened in their day, but how different situations consistently affect their psychological wellbeing, enabling more intentional lifestyle choices.
A performance enhancement system for professionals looking to maximize their cognitive efficiency. By mapping mood patterns against work output, users identify their optimal emotional states for different task types - discovering when they're most creative, analytical, or communicative, allowing for strategic scheduling of activities around natural energy and mood fluctuations rather than fighting against them.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.