/web-app-features

How to Add Personalized Journal with Mood Tracking to Your Web App

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

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add Personalized Journal with Mood Tracking to Your Web App

Adding a Personalized Journal with Mood Tracking to Your Web App

 

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.

 

Architecture Overview

 

The Three Pillars of a Journal + Mood System

 

  • A flexible data model that accommodates unstructured journal content alongside structured mood data
  • An intuitive UI that encourages daily use without feeling clinical
  • Thoughtful analysis tools that reveal patterns without overwhelming users

 

Let me walk you through implementing this feature end-to-end, with code examples for each component.

 

1. Setting Up the Data Model

 

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")
}

 

2. Building the Backend API

 

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;

 

3. Crafting the Frontend Experience

 

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();
              }}
            >
              &times;
            </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;

 

4. Building the Analytics Dashboard

 

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;

 

5. Tying It All Together

 

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;

 

6. Advanced Features and Considerations

 

Enhancing Your Journal with Smart Features

 

  • Sentiment Analysis: Analyze journal text to automatically suggest mood scores
  • Pattern Detection: Identify correlations between activities and mood changes
  • Reminder System: Nudge users to journal during specified times
  • Export Functionality: Allow users to export their journal as PDF or text
  • Journaling Prompts: Suggest writing prompts based on detected patterns

 

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

 

7. Performance and Security Considerations

 

Keeping Data Safe and Fast

 

  • Encryption: Store journal entries and mood data with proper encryption at rest
  • Pagination: Implement efficient pagination for journal entries to handle years of data
  • Caching: Cache analytical results to avoid recalculating common time periods
  • Rate Limiting: Prevent abuse with sensible rate limits on journal and mood submissions
  • Data Retention: Consider offering data export and deletion options for user privacy

 

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

 

Conclusion

 

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:

 

  • Start simple: Begin with basic journaling, then add mood tracking, and finally analytics
  • Prioritize the UX: Journaling is a personal experience—design your interface to feel private and reflective
  • Get feedback early: Different users have different journaling habits; test with diverse users
  • Consider mobile: Many users prefer to journal from their phones before bed or first thing in the morning

 

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.

Ship Personalized Journal with Mood Tracking 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 Personalized Journal with Mood Tracking Usecases

Explore the top 3 personalized journal use cases with mood tracking to enhance your web app experience.

Holistic Mental Health Management

 

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.

 

Personal Growth & Self-Awareness Development

 

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.

 

Productivity Optimization Platform

 

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.

 


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

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.