Skip to main content

MoStreaks

"The Motivator" — "I keep you consistent"

Status: ✅ Built

MoStreaks tracks workout consistency with a 48-hour window for maintaining streaks, and provides motivational messaging.


Purpose

  • Track consecutive workout days
  • Maintain streak with 48-hour window (allows rest days)
  • Provide motivational messages based on streak status
  • Auto-update on workout completion

Implementation

Code Location

/lib/mo-self/history/streaks.ts

Key Functions

// Get current streak data
export async function getStreak(userId: string): Promise<StreakData>

// Update streak on workout completion (called automatically)
export async function updateStreakOnWorkout(userId: string): Promise<StreakData>

// Get streak statistics
export async function getStreakStats(userId: string): Promise<StreakStats>

// Get motivational message based on streak
export function getStreakMessage(streak: StreakData): StreakMessage

Usage

import { getStreak, updateStreakOnWorkout } from '@/lib/mo-self';

// Get current streak
const streak = await getStreak(userId);
console.log(`Current streak: ${streak.currentStreak} days`);

// Auto-update on workout (called in session complete handler)
const updatedStreak = await updateStreakOnWorkout(userId);

Data Model

interface StreakData {
userId: string;
currentStreak: number;
longestStreak: number;
lastWorkoutDate: Date | null;
streakStartDate: Date | null;
totalWorkouts: number;

// Computed
isActive: boolean; // Has valid streak
hoursRemaining: number; // Hours until streak breaks
status: StreakStatus;
}

type StreakStatus =
| 'none' // No streak (0 days)
| 'starting' // 1-2 days
| 'building' // 3-6 days
| 'strong' // 7-13 days
| 'impressive' // 14-29 days
| 'legendary'; // 30+ days

interface StreakMessage {
title: string;
message: string;
emoji: string;
}

Database Table

CREATE TABLE streaks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) UNIQUE,
current_streak INTEGER DEFAULT 0,
longest_streak INTEGER DEFAULT 0,
last_workout_date DATE,
streak_start_date DATE,
total_workouts INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

48-Hour Window

Streaks use a 48-hour window instead of daily:

Day 1: Workout ✓ (streak = 1)
Day 2: Rest day (streak maintained)
Day 3: Workout ✓ (streak = 2)
Day 4: Rest day (streak maintained)
Day 5: No workout (streak breaks after 48hrs from Day 3)

This allows for proper rest while maintaining motivation.

Logic

const STREAK_WINDOW_HOURS = 48;

function isStreakActive(lastWorkoutDate: Date): boolean {
const hoursSinceLastWorkout =
(Date.now() - lastWorkoutDate.getTime()) / (1000 * 60 * 60);
return hoursSinceLastWorkout <= STREAK_WINDOW_HOURS;
}

Motivational Messages

StatusDaysExample Message
none0"Start your journey today!"
starting1-2"Great start! Keep it going!"
building3-6"Building momentum!"
strong7-13"One week strong!"
impressive14-29"Two weeks of dedication!"
legendary30+"Legendary consistency!"

API Endpoints

GET /api/streaks

Returns streak data with stats and message.

{
"currentStreak": 7,
"longestStreak": 14,
"lastWorkoutDate": "2024-12-21",
"totalWorkouts": 45,
"isActive": true,
"hoursRemaining": 36,
"status": "strong",
"message": {
"title": "One Week Strong!",
"message": "You've worked out 7 days in a row. Keep pushing!",
"emoji": "💪"
}
}

Integration Points

Receives from:

  • /api/ppl/session PATCH (auto-update on workout complete)

Provides to:

  • Dashboard (streak display)
  • MoAlerts (streak warning notifications - future)

Auto-Hook

Streak is automatically updated when a workout is completed:

// In /api/ppl/session PATCH handler
const streak = await updateStreakOnWorkout(user.id);

return NextResponse.json({
session,
metrics,
streak
});

Streak Recovery

If a streak breaks, users start fresh at 0. There's no "recovery" mechanism - this encourages consistent training.

Future enhancement: Allow one "streak save" per month for illness/travel.