/** * Product-agnostic celebration engine. * * Provides milestone triggers, haptic configs, confetti, sounds, * and positive reinforcement messages. * Pure client-side TS — no backend dependency. */ import type { Celebration, CelebrationConfig, CelebrationEngine, CelebrationTrigger, } from './types.js'; const DEFAULT_CELEBRATIONS: Record = { task_completed: { id: 'task_completed', title: 'Task Done!', body: 'Great work completing that task!', emoji: '✅', hapticType: 'light', confetti: false, sound: 'chime', }, streak_continued: { id: 'streak_continued', title: 'Streak Alive!', body: 'You kept your streak going — amazing consistency!', emoji: '🔥', hapticType: 'medium', confetti: false, sound: 'chime', }, streak_milestone: { id: 'streak_milestone', title: 'Streak Milestone!', body: 'What an incredible streak — you are unstoppable!', emoji: '⭐', hapticType: 'heavy', confetti: true, sound: 'success', }, achievement_unlocked: { id: 'achievement_unlocked', title: 'Achievement Unlocked!', body: 'You just unlocked a new achievement!', emoji: '🏆', hapticType: 'heavy', confetti: true, sound: 'success', }, level_up: { id: 'level_up', title: 'Level Up!', body: 'You reached a new level — keep going!', emoji: '🚀', hapticType: 'heavy', confetti: true, sound: 'level_up', }, personal_best: { id: 'personal_best', title: 'Personal Best!', body: 'You just set a new personal record!', emoji: '🥇', hapticType: 'heavy', confetti: true, sound: 'success', }, milestone_reached: { id: 'milestone_reached', title: 'Milestone Reached!', body: 'Another milestone in the books — well done!', emoji: '🎯', hapticType: 'medium', confetti: true, sound: 'success', }, goal_completed: { id: 'goal_completed', title: 'Goal Complete!', body: 'You achieved your goal — celebrate this win!', emoji: '🎉', hapticType: 'heavy', confetti: true, sound: 'success', }, first_action: { id: 'first_action', title: 'First Step!', body: 'Every journey begins with a single step — great start!', emoji: '👣', hapticType: 'medium', confetti: true, sound: 'chime', }, halfway: { id: 'halfway', title: 'Halfway There!', body: "You're halfway through — the finish line is in sight!", emoji: '⏳', hapticType: 'medium', confetti: false, sound: 'chime', }, session_completed: { id: 'session_completed', title: 'Session Complete!', body: 'Another session done — you showed up and that matters!', emoji: '💪', hapticType: 'medium', confetti: false, sound: 'success', }, session_started: { id: 'session_started', title: 'Session Started!', body: 'You just started — showing up is the hardest part!', emoji: '🌟', hapticType: 'light', confetti: false, sound: 'none', }, }; const POSITIVE_MESSAGES: string[] = [ "You're doing amazing!", 'Keep it up — every step counts!', "Look at you go — you're incredible!", 'Your dedication is inspiring!', 'Consistency is your superpower!', "You're making real progress!", 'One step at a time — and you nailed it!', 'Be proud of how far you have come!', ]; const POSITIVE_INCOMPLETE_MESSAGES: string[] = [ 'Every bit of progress counts!', 'You showed up — that takes courage!', "It's okay to take a break — you'll come back stronger!", 'Progress, not perfection!', 'You started, and that matters most!', 'Rest is part of the journey!', "You've done more than you think!", 'Tomorrow is another opportunity!', ]; const TIMED_MILESTONES = [ { fraction: 0.25, id: 'timed_25' }, { fraction: 0.5, id: 'timed_50' }, { fraction: 0.75, id: 'timed_75' }, { fraction: 1.0, id: 'timed_100' }, ]; const FALLBACK_CELEBRATION: Celebration = { id: 'generic', title: 'Nice!', body: 'Keep up the great work!', emoji: '🎉', hapticType: 'light', confetti: false, sound: 'chime', }; export function createCelebrationEngine(config?: CelebrationConfig): CelebrationEngine { const customTriggers = config?.customTriggers ?? {}; function getCelebration(trigger: CelebrationTrigger | string): Celebration { if (trigger in customTriggers) return customTriggers[trigger]; if (trigger in DEFAULT_CELEBRATIONS) return DEFAULT_CELEBRATIONS[trigger as CelebrationTrigger]; return { ...FALLBACK_CELEBRATION, id: trigger }; } function getTimedCelebrations( elapsedMs: number, targetMs: number, shownIds: Set ): Celebration[] { if (targetMs <= 0) return []; const progress = elapsedMs / targetMs; const results: Celebration[] = []; for (const milestone of TIMED_MILESTONES) { if (progress >= milestone.fraction && !shownIds.has(milestone.id)) { const percent = Math.round(milestone.fraction * 100); results.push({ id: milestone.id, title: `${percent}% Complete!`, body: getPositiveMessage(percent), emoji: milestone.fraction === 1.0 ? '🎉' : '⏳', hapticType: milestone.fraction === 1.0 ? 'heavy' : 'medium', confetti: milestone.fraction >= 0.75, sound: milestone.fraction === 1.0 ? 'success' : 'chime', }); } } return results; } function isPersonalBest(current: number, previous: number): boolean { return current > previous && previous >= 0; } function getPositiveMessage(progressPercent: number): string { const clamped = Math.max(0, Math.min(100, progressPercent)); const index = Math.floor((clamped / 100) * (POSITIVE_MESSAGES.length - 1)); return POSITIVE_MESSAGES[index]; } function getPositiveIncompleteMessage(progressPercent: number): string { const clamped = Math.max(0, Math.min(100, progressPercent)); const index = Math.floor((clamped / 100) * (POSITIVE_INCOMPLETE_MESSAGES.length - 1)); return POSITIVE_INCOMPLETE_MESSAGES[index]; } return { getCelebration, getTimedCelebrations, isPersonalBest, getPositiveMessage, getPositiveIncompleteMessage, }; }