diff --git a/app/_offline/page.tsx b/app/_offline/page.tsx index cec811b..7ac04e5 100644 --- a/app/_offline/page.tsx +++ b/app/_offline/page.tsx @@ -3,7 +3,7 @@ export default function OfflinePage() { return (
-

You're offline

+

You're offline

InnerHue needs an internet connection for full experience. Cached content may still be visible. diff --git a/app/about/page.tsx b/app/about/page.tsx index a0a9be0..f7b8b29 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -74,7 +74,7 @@ export default function AboutUs() { Our Mission

- In a world that never stops moving, it's easy to lose touch with how we really feel. + In a world that never stops moving, it's easy to lose touch with how we really feel. InnerHue was born from a simple belief: acknowledging your emotions is the first step towards well-being. We aim to provide a safe, beautiful, and intuitive space where you can pause, reflect, and find clarity amidst the chaos.

@@ -92,7 +92,7 @@ export default function AboutUs() {

InnerHue uses color psychology and mood tracking to help you visualize your emotional state. By selecting your current mood, you unlock personalized journal prompts, inspirational quotes, and - curated resources designed to support exactly how you're feeling right now. + curated resources designed to support exactly how you're feeling right now.

@@ -106,8 +106,8 @@ export default function AboutUs() {

For Everyone

- Whether you're feeling overwhelmed, ecstatic, or somewhere in between, InnerHue is here for you. - We believe that every emotion is valid and deserves to be heard. There is no "right" or "wrong" way to feelβ€”only + Whether you're feeling overwhelmed, ecstatic, or somewhere in between, InnerHue is here for you. + We believe that every emotion is valid and deserves to be heard. There is no "right" or "wrong" way to feelβ€”only your unique human experience.

@@ -118,7 +118,7 @@ export default function AboutUs() { className="md:col-span-2 text-center py-12" >

- "To feel is to be human." + "To feel is to be human."

Thank you for being part of our journey. diff --git a/app/globals.css b/app/globals.css index 2a6b7db..cf3f2d3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -164,4 +164,22 @@ [data-framer-motion], [style*="transform"] { transition-property: background-color, border-color, color, box-shadow; +} + +/* Custom scrollbar for streak detail timeline */ +.custom-scrollbar::-webkit-scrollbar { + width: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 030cc30..775efcd 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -99,7 +99,7 @@ export default function Home() { }, []); return ( - -

+

@@ -152,7 +152,7 @@ export default function Home() {

-
- + {/* Footer */} diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index 77fa727..b3d4e14 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -2,11 +2,11 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { LoginSchema } from "@/lib/validation"; +import { LoginSchema } from "@/lib/validation"; import { z } from "zod"; import { useState } from "react"; import Link from "next/link"; -import { toast } from "sonner"; +import { toast } from "sonner"; type LoginFormData = z.infer; @@ -26,15 +26,15 @@ export default function LoginForm() { try { const toastId = toast.loading("Logging in..."); console.log("Logging in with:", data); - + // Simulate API call await new Promise((resolve) => setTimeout(resolve, 2000)); - + toast.dismiss(toastId); toast.success("Welcome back!", { description: "You have successfully logged in.", }); - + } catch (error) { toast.error("Login failed", { description: "Please check your email and password.", @@ -43,8 +43,8 @@ export default function LoginForm() { }; return ( -
@@ -96,9 +96,9 @@ export default function LoginForm() { {/* Footer Link */}

- Don't have an account?{" "} - Create one now diff --git a/components/SimpleLangFlowChatbot.tsx b/components/SimpleLangFlowChatbot.tsx index c56e075..2a9f144 100644 --- a/components/SimpleLangFlowChatbot.tsx +++ b/components/SimpleLangFlowChatbot.tsx @@ -143,10 +143,10 @@ export default function SimpleLangFlowChatbot({ onEmotionDetected, onAutoNavigat {!botResponse ? (

- Hey there! πŸ‘‹ I'm your emotion companion. + Hey there! πŸ‘‹ I'm your emotion companion.

- Tell me how you're feeling and I'll help you explore your emotions! + Tell me how you're feeling and I'll help you explore your emotions!

) : ( diff --git a/components/StreakCard.tsx b/components/StreakCard.tsx new file mode 100644 index 0000000..ddf04d0 --- /dev/null +++ b/components/StreakCard.tsx @@ -0,0 +1,419 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { Flame, Trophy, Calendar, X, Clock, TrendingUp } from 'lucide-react'; +import { useState, useEffect, useMemo } from 'react'; +import { + loadStreakData, + recordDailyStreak, + recalculateStreakFromHistory, + hasLoggedToday as checkLoggedToday, + type StreakData, +} from '@/lib/streakService'; +import { + generateNudges, + dismissNudge, + type Nudge, +} from '@/lib/nudgeService'; +import { useMoodStore, type MoodEntry } from '@/lib/useMoodStore'; + +/* ─── Streak Nav Icon (Navbar Button) ─── */ +export function StreakNavIcon() { + const [streakData, setStreakData] = useState(null); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const moodHistory = useMoodStore((s) => s.moodHistory); + + // On mount: recalculate streak from mood history & auto-record today's visit + useEffect(() => { + if (moodHistory.length > 0) { + recalculateStreakFromHistory(moodHistory); + } + + // Auto-record daily streak on page load (once per calendar day) + const updated = recordDailyStreak(); + setStreakData(updated); + }, [moodHistory]); + + if (!streakData) return null; + + return ( + <> + {/* Nav Icon Button */} + setIsDetailOpen(true)} + className="relative p-2 rounded-full bg-white/5 backdrop-blur-xl hover:bg-white/10 transition-all duration-300 border border-white/10 flex items-center gap-1.5" + title="Reflection Streak" + aria-label={`Reflection Streak: ${streakData.currentStreak} days`} + > + 0 + ? { scale: [1, 1.15, 1] } + : {} + } + transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }} + > + 0 ? 'text-orange-400' : 'text-white' + }`} + /> + + + {/* Streak count badge */} + {streakData.currentStreak > 0 && ( + + {streakData.currentStreak} + + )} + + + {/* Detail Modal */} + + {isDetailOpen && ( + setIsDetailOpen(false)} + /> + )} + + + ); +} + +/* ─── Expanded Streak Detail Modal ─── */ +function StreakDetailModal({ + streakData, + moodHistory, + onClose, +}: { + streakData: StreakData; + moodHistory: MoodEntry[]; + onClose: () => void; +}) { + const [nudges, setNudges] = useState([]); + const [showNudge, setShowNudge] = useState(true); + + useEffect(() => { + const logged = checkLoggedToday(); + const nudgeList = generateNudges(streakData, logged); + setNudges(nudgeList); + }, [streakData]); + + const handleDismissNudge = () => { + dismissNudge(); + setShowNudge(false); + }; + + const topNudge = nudges.length > 0 && showNudge ? nudges[0] : null; + + // Mood data enrichment: map mood ID to emoji/label + const moodMap: Record = useMemo(() => { + const map: Record = {}; + const defaultMoods: Record = { + happy: { emoji: '😊', name: 'Happy', color: '#FFD93D' }, + sad: { emoji: '😒', name: 'Sad', color: '#42A5F5' }, + anxious: { emoji: '😰', name: 'Anxious', color: '#FF7043' }, + excited: { emoji: '🀩', name: 'Excited', color: '#AB47BC' }, + calm: { emoji: '😌', name: 'Calm', color: '#66BB6A' }, + angry: { emoji: '😑', name: 'Angry', color: '#EF5350' }, + confused: { emoji: 'πŸ˜•', name: 'Confused', color: '#FFA726' }, + grateful: { emoji: 'πŸ™', name: 'Grateful', color: '#26A69A' }, + lonely: { emoji: 'πŸ˜”', name: 'Lonely', color: '#7E57C2' }, + hopeful: { emoji: '🌟', name: 'Hopeful', color: '#FFCA28' }, + stressed: { emoji: '😀', name: 'Stressed', color: '#FF5722' }, + peaceful: { emoji: 'πŸ•ŠοΈ', name: 'Peaceful', color: '#4FC3F7' }, + energized: { emoji: '⚑', name: 'Energized', color: '#FFEB3B' }, + overwhelmed: { emoji: '🀯', name: 'Overwhelmed', color: '#F06292' }, + content: { emoji: '😊', name: 'Content', color: '#AED581' }, + frustrated: { emoji: '😠', name: 'Frustrated', color: '#FF8A65' }, + inspired: { emoji: 'πŸ’‘', name: 'Inspired', color: '#FFD740' }, + melancholy: { emoji: '🌧️', name: 'Melancholy', color: '#90A4AE' }, + motivated: { emoji: 'πŸ”₯', name: 'Motivated', color: '#FF6D00' }, + vulnerable: { emoji: 'πŸ₯Ί', name: 'Vulnerable', color: '#F8BBD9' }, + empowered: { emoji: 'πŸ’ͺ', name: 'Empowered', color: '#6A1B9A' }, + nostalgic: { emoji: 'πŸ“Έ', name: 'Nostalgic', color: '#D4A574' }, + proud: { emoji: '😀', name: 'Proud', color: '#FF9800' }, + curious: { emoji: 'πŸ€”', name: 'Curious', color: '#9C27B0' }, + bored: { emoji: 'πŸ˜‘', name: 'Bored', color: '#607D8B' }, + surprised: { emoji: '😲', name: 'Surprised', color: '#FF5722' }, + creative: { emoji: '🎨', name: 'Creative', color: '#FF7043' }, + determined: { emoji: '😀', name: 'Determined', color: '#3F51B5' }, + playful: { emoji: '😜', name: 'Playful', color: '#FF4081' }, + dreamy: { emoji: '😴', name: 'Dreamy', color: '#9FA8DA' }, + silly: { emoji: 'πŸ€ͺ', name: 'Silly', color: '#FFC107' }, + }; + Object.entries(defaultMoods).forEach(([id, data]) => { + map[id] = { emoji: data.emoji, label: data.name, color: data.color }; + }); + return map; + }, []); + + // Recent mood entries (last 14 days) for timeline, sorted newest first + const recentEntries = useMemo(() => { + const twoWeeksAgo = new Date(); + twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); + return [...moodHistory] + .filter(e => new Date(e.timestamp) >= twoWeeksAgo) + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + }, [moodHistory]); + + // Group entries by date + const groupedEntries = useMemo(() => { + const groups: Record = {}; + recentEntries.forEach(entry => { + const d = new Date(entry.timestamp); + const key = d.toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + if (!groups[key]) groups[key] = []; + groups[key].push(entry); + }); + return groups; + }, [recentEntries]); + + const formatTime = (ts: string) => { + return new Date(ts).toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + }; + + return ( + + {/* Backdrop */} +
+ + {/* Content */} + e.stopPropagation()} + > + {/* Header gradient */} +
+ + +
+ + + +

Reflection Streak

+
+ + {/* Streak Stats Grid */} +
+
+
+ +
+

{streakData.currentStreak}

+

Current

+
+
+
+ +
+

{streakData.longestStreak}

+

Longest

+
+
+
+ +
+

+ {streakData.lastLoggedDate + ? new Date(streakData.lastLoggedDate + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + : 'β€”'} +

+

Last Log

+
+
+ + {/* Streak continuity dots (last 7 days) */} +
+ {Array.from({ length: 7 }).map((_, i) => { + const date = new Date(); + date.setDate(date.getDate() - (6 - i)); + const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + const isLogged = streakData.streakDates.includes(dateStr); + const isToday = i === 6; + const dayLabel = date.toLocaleDateString('en-US', { weekday: 'short' }).charAt(0); + + return ( +
+ + {dayLabel} +
+ ); + })} +
+ + {/* Streak info text */} +
+ +

+ A streak is maintained by logging at least one mood per calendar day. +

+
+ + {/* Smart Nudge */} + + {topNudge && ( + +
+ +
+ {topNudge.emoji} +

{topNudge.message}

+
+
+
+ )} +
+
+ + {/* Mood Timeline */} +
+
+ +

Mood Timeline

+
+ + {Object.keys(groupedEntries).length === 0 ? ( + /* Empty state */ + +
🌿
+

No reflections yet

+

Select a mood above to start your first entry

+
+ ) : ( + /* Vertical Timeline */ +
+ {/* Vertical line */} +
+ + {Object.entries(groupedEntries).map(([dateLabel, entries], groupIdx) => ( +
+ {/* Date label */} + +
+ +
+ {dateLabel} +
+ + {/* Entries within the day */} + {entries.map((entry, entryIdx) => { + const moodKey = entry.emotion || entry.mood; + const moodInfo = moodMap[moodKey] || { emoji: 'πŸ«₯', label: moodKey, color: '#888' }; + + return ( + +
+ {/* Timeline connector dot */} +
+ +
+ {moodInfo.emoji} +
+
+ + {moodInfo.label} + + + {formatTime(entry.timestamp)} + +
+ {entry.date && ( +

+ {entry.date} +

+ )} +
+
+
+ + ); + })} +
+ ))} +
+ )} +
+ + + ); +} diff --git a/components/SuggestionPanel.tsx b/components/SuggestionPanel.tsx index e2322e3..ef801d0 100644 --- a/components/SuggestionPanel.tsx +++ b/components/SuggestionPanel.tsx @@ -43,9 +43,9 @@ export function SuggestionPanel({ suggestions, mood, onRefresh, isRefreshing = f const cardVariants = { hidden: { opacity: 0, y: 30, scale: 0.95 }, - visible: { - opacity: 1, - y: 0, + visible: { + opacity: 1, + y: 0, scale: 1, transition: { type: "spring" as const, stiffness: 100, damping: 15 } }, @@ -58,14 +58,14 @@ export function SuggestionPanel({ suggestions, mood, onRefresh, isRefreshing = f return ( - {/* Header with animated gradient */} - - +
{[...Array(6)].map((_, i) => ( - - " + "
@@ -196,7 +196,7 @@ export function SuggestionPanel({ suggestions, mood, onRefresh, isRefreshing = f
-
"{suggestions.quote}"
+
"{suggestions.quote}"
{suggestions.author} diff --git a/components/landing/Hero.tsx b/components/landing/Hero.tsx index 4cc4540..6dc35c0 100644 --- a/components/landing/Hero.tsx +++ b/components/landing/Hero.tsx @@ -3,10 +3,10 @@ import { motion } from 'framer-motion'; import { ArrowRight, Sparkles } from 'lucide-react'; import { LandingOrb } from './LandingOrb'; -import { usePageTransition } from '@/components/TransitionProvider'; +import { useRouter } from 'next/navigation'; export function Hero() { - const { startTransition, isTransitioning } = usePageTransition(); + const router = useRouter(); // Animation variants for staggered fade-in const containerVariants = { @@ -93,9 +93,8 @@ export function Hero() { {/* Primary CTA Button - Glassmorphic Style with Custom Transition */} startTransition('/explore')} - disabled={isTransitioning} - whileHover={{ + onClick={() => router.push('/explore')} + whileHover={{ scale: 1.08, boxShadow: '0 0 60px rgba(139, 92, 246, 0.6)', }} @@ -112,20 +111,20 @@ export function Hero() { disabled:opacity-50 disabled:cursor-not-allowed" > {/* Animated gradient background on hover */} - - + {/* Shimmer effect */} - + Start Reflecting ): CustomMood { const customMoods = this.getCustomMoods(); - + const newMood: CustomMood = { ...mood, id: `custom_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, @@ -56,7 +56,7 @@ export const CustomMoodStorage = { }; customMoods.push(newMood); - + try { localStorage.setItem(CUSTOM_MOODS_STORAGE_KEY, JSON.stringify(customMoods)); // Dispatch a custom event to notify components of the change @@ -75,7 +75,7 @@ export const CustomMoodStorage = { deleteCustomMood(moodId: string): boolean { const customMoods = this.getCustomMoods(); const filteredMoods = customMoods.filter(mood => mood.id !== moodId); - + if (filteredMoods.length === customMoods.length) { return false; // Mood not found } @@ -95,7 +95,7 @@ export const CustomMoodStorage = { */ moodNameExists(name: string): boolean { const customMoods = this.getCustomMoods(); - return customMoods.some(mood => + return customMoods.some(mood => mood.name.toLowerCase() === name.toLowerCase() ); }, @@ -143,19 +143,12 @@ export function getCombinedMoods(defaultMoods: Mood[]): Mood[] { * Hook for listening to custom mood changes */ export function useCustomMoods(defaultMoods: Mood[] = []) { - if (typeof window === 'undefined') { - return { - allMoods: defaultMoods, - customMoods: [], - addCustomMood: () => Promise.reject(new Error('Not available on server')), - deleteCustomMood: () => false, - refreshMoods: () => {} - }; - } - const [customMoods, setCustomMoods] = useState([]); + const isClient = typeof window !== 'undefined'; useEffect(() => { + if (!isClient) return; + // Load initial custom moods const loadCustomMoods = () => { const moods = CustomMoodStorage.getCustomMoods(); @@ -174,7 +167,17 @@ export function useCustomMoods(defaultMoods: Mood[] = []) { return () => { window.removeEventListener('customMoodsUpdated', handleCustomMoodsUpdate as EventListener); }; - }, []); + }, [isClient]); + + if (!isClient) { + return { + allMoods: defaultMoods, + customMoods: [], + addCustomMood: () => Promise.reject(new Error('Not available on server')), + deleteCustomMood: () => false, + refreshMoods: () => { } + }; + } const addCustomMood = async (moodData: Omit): Promise => { return CustomMoodStorage.saveCustomMood(moodData); diff --git a/lib/nudgeService.ts b/lib/nudgeService.ts new file mode 100644 index 0000000..71decde --- /dev/null +++ b/lib/nudgeService.ts @@ -0,0 +1,143 @@ +/** + * Smart Nudge Service β€” Context-Aware, Non-Intrusive Nudges + * + * Rules-based nudge system: + * - No mood logged today β†’ soft reminder + * - Streak about to break (logged yesterday but not today + evening) β†’ encouragement + * - Streak milestone reached β†’ positive reinforcement + * - Respects user inactivity (won't show if dismissed recently) + */ + +import type { StreakData } from './streakService'; + +export interface Nudge { + id: string; + type: 'reminder' | 'encouragement' | 'celebration' | 'milestone'; + message: string; + emoji: string; + priority: number; // Higher = more important, show first +} + +const NUDGE_DISMISS_KEY = 'innerhue-nudge-dismissed'; + +// Milestone thresholds for celebration nudges +const MILESTONES = [3, 7, 14, 21, 30, 50, 75, 100]; + +function getLocalHour(): number { + return new Date().getHours(); +} + +function isDismissedToday(): boolean { + if (typeof window === 'undefined') return false; + try { + const dismissed = localStorage.getItem(NUDGE_DISMISS_KEY); + if (!dismissed) return false; + const today = new Date().toDateString(); + return dismissed === today; + } catch { + return false; + } +} + +export function dismissNudge(): void { + if (typeof window === 'undefined') return; + localStorage.setItem(NUDGE_DISMISS_KEY, new Date().toDateString()); +} + +/** + * Generate context-aware nudges based on streak data. + * Returns an array of nudges sorted by priority (highest first). + * Returns empty array if nudges are dismissed or not applicable. + */ +export function generateNudges(streakData: StreakData, hasLoggedToday: boolean): Nudge[] { + // Respect dismissal β€” user swiped away nudges for today + if (isDismissedToday()) return []; + + const nudges: Nudge[] = []; + const hour = getLocalHour(); + + // 1. Streak milestone celebration (highest priority) + if (hasLoggedToday && MILESTONES.includes(streakData.currentStreak)) { + nudges.push({ + id: `milestone-${streakData.currentStreak}`, + type: 'milestone', + message: getMilestoneMessage(streakData.currentStreak), + emoji: getMilestoneEmoji(streakData.currentStreak), + priority: 100, + }); + } + + // 2. Streak about to break β€” logged yesterday, not today, and it's afternoon/evening + if (!hasLoggedToday && streakData.currentStreak > 0 && hour >= 14) { + nudges.push({ + id: 'streak-warning', + type: 'encouragement', + message: `Your ${streakData.currentStreak}-day streak is still going! Take a moment to reflect before the day ends.`, + emoji: '⏰', + priority: 80, + }); + } + + // 3. Gentle reminder if no mood logged today (only show from 10am onwards) + if (!hasLoggedToday && hour >= 10) { + const reminderMessages = [ + "How are you feeling right now? A quick check-in can make all the difference.", + "Your emotions matter. Take a moment to acknowledge how you're feeling today.", + "A small reflection today keeps emotional awareness growing.", + "Pause, breathe, and check in with yourself. How's your inner world?", + ]; + const msgIndex = new Date().getDate() % reminderMessages.length; + + nudges.push({ + id: 'daily-reminder', + type: 'reminder', + message: reminderMessages[msgIndex], + emoji: 'πŸ’­', + priority: 40, + }); + } + + // 4. Positive reinforcement after logging (show celebration briefly) + if (hasLoggedToday && streakData.currentStreak > 1 && !MILESTONES.includes(streakData.currentStreak)) { + nudges.push({ + id: 'positive-reinforcement', + type: 'celebration', + message: getStreakEncouragement(streakData.currentStreak), + emoji: 'πŸ”₯', + priority: 60, + }); + } + + // Sort by priority descending, return only the top nudge to avoid spam + return nudges.sort((a, b) => b.priority - a.priority); +} + +function getMilestoneMessage(streak: number): string { + const messages: Record = { + 3: "3 days of reflection! You're building a beautiful habit. 🌱", + 7: "One full week! Your emotional awareness is growing stronger each day. 🌟", + 14: "Two weeks of consistent reflection β€” that's real commitment to yourself! πŸ’Ž", + 21: "21 days β€” they say that's what it takes to build a habit. Incredible! πŸ†", + 30: "A full month of emotional check-ins! You're truly dedicated to self-growth. πŸŽ‰", + 50: "50 days! Half a century of daily reflections. That's extraordinary! 🌈", + 75: "75 days! You're a reflection champion. Your emotional intelligence is soaring! ⭐", + 100: "100 DAYS! An incredible milestone. You've transformed your emotional awareness! 🎊", + }; + return messages[streak] || `Amazing! ${streak} days of reflection!`; +} + +function getMilestoneEmoji(streak: number): string { + if (streak >= 100) return '🎊'; + if (streak >= 50) return '🌈'; + if (streak >= 21) return 'πŸ†'; + if (streak >= 7) return '🌟'; + return '🌱'; +} + +function getStreakEncouragement(streak: number): string { + if (streak >= 30) return "You're on fire! Consistency is your superpower."; + if (streak >= 14) return "Two weeks strong! Your dedication is inspiring."; + if (streak >= 7) return "A full week of reflection! Keep the momentum going."; + if (streak >= 3) return "You're building momentum! Every day counts."; + return "Great job checking in today! Keep it going."; +} diff --git a/lib/streakService.ts b/lib/streakService.ts new file mode 100644 index 0000000..186968f --- /dev/null +++ b/lib/streakService.ts @@ -0,0 +1,195 @@ +/** + * Streak Service β€” Mood Reflection Streak Calculator + * + * A streak = at least one mood logged per calendar day. + * Streak resets if a full calendar day is missed. + * Only one streak increment per calendar day (prevents duplicates on repeated visits). + */ + +export interface StreakData { + currentStreak: number; + longestStreak: number; + lastLoggedDate: string | null; // ISO date string (YYYY-MM-DD) + streakDates: string[]; // Array of ISO date strings where moods were logged +} + +const STREAK_STORAGE_KEY = 'innerhue-streak-data'; + +// Timezone-safe: get local calendar date as YYYY-MM-DD +function getLocalDateString(date: Date = new Date()): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +// Calculate difference in calendar days between two YYYY-MM-DD strings +function daysBetween(dateA: string, dateB: string): number { + const a = new Date(dateA + 'T00:00:00'); + const b = new Date(dateB + 'T00:00:00'); + const diffMs = Math.abs(a.getTime() - b.getTime()); + return Math.round(diffMs / (1000 * 60 * 60 * 24)); +} + +function getDefaultStreakData(): StreakData { + return { + currentStreak: 0, + longestStreak: 0, + lastLoggedDate: null, + streakDates: [], + }; +} + +/** Load persisted streak data from localStorage */ +export function loadStreakData(): StreakData { + if (typeof window === 'undefined') return getDefaultStreakData(); + + try { + const raw = localStorage.getItem(STREAK_STORAGE_KEY); + if (!raw) return getDefaultStreakData(); + const parsed = JSON.parse(raw) as StreakData; + return { + currentStreak: parsed.currentStreak ?? 0, + longestStreak: parsed.longestStreak ?? 0, + lastLoggedDate: parsed.lastLoggedDate ?? null, + streakDates: parsed.streakDates ?? [], + }; + } catch { + return getDefaultStreakData(); + } +} + +/** Save streak data to localStorage */ +export function saveStreakData(data: StreakData): void { + if (typeof window === 'undefined') return; + localStorage.setItem(STREAK_STORAGE_KEY, JSON.stringify(data)); +} + +/** + * Record today's mood reflection in the streak. + * Only increments once per calendar day. + * Returns the updated StreakData. + */ +export function recordDailyStreak(): StreakData { + const data = loadStreakData(); + const today = getLocalDateString(); + + // Already logged today β€” no duplicate increment + if (data.lastLoggedDate === today) { + return data; + } + + // Determine if streak continues or resets + if (data.lastLoggedDate) { + const gap = daysBetween(data.lastLoggedDate, today); + if (gap === 1) { + // Consecutive day β€” continue streak + data.currentStreak += 1; + } else if (gap === 0) { + // Same day (shouldn't reach here due to early return, but safety) + return data; + } else { + // Missed day(s) β€” reset streak + data.currentStreak = 1; + } + } else { + // First ever log + data.currentStreak = 1; + } + + // Update longest streak + if (data.currentStreak > data.longestStreak) { + data.longestStreak = data.currentStreak; + } + + // Record the date + data.lastLoggedDate = today; + if (!data.streakDates.includes(today)) { + data.streakDates.push(today); + } + + // Keep only last 90 days of streak dates to limit storage + if (data.streakDates.length > 90) { + data.streakDates = data.streakDates.slice(-90); + } + + saveStreakData(data); + return data; +} + +/** + * Recalculate streak from mood history entries. + * Useful after hydration or initial load. + */ +export function recalculateStreakFromHistory( + moodHistory: Array<{ timestamp: string }> +): StreakData { + if (!moodHistory || moodHistory.length === 0) { + return getDefaultStreakData(); + } + + // Extract unique calendar dates from mood history + const uniqueDates = Array.from( + new Set( + moodHistory.map(entry => getLocalDateString(new Date(entry.timestamp))) + ) + ).sort(); + + if (uniqueDates.length === 0) return getDefaultStreakData(); + + // Walk backwards from most recent date to count current streak + const today = getLocalDateString(); + const lastDate = uniqueDates[uniqueDates.length - 1]; + + // If last logged date is more than 1 day before today, streak is broken + const gapFromToday = daysBetween(lastDate, today); + + let currentStreak = 0; + if (gapFromToday <= 1) { + // Streak is still active (logged today or yesterday) + currentStreak = 1; + for (let i = uniqueDates.length - 2; i >= 0; i--) { + const gap = daysBetween(uniqueDates[i], uniqueDates[i + 1]); + if (gap === 1) { + currentStreak++; + } else { + break; + } + } + } + + // Find longest streak across all history + let longestStreak = 1; + let tempStreak = 1; + for (let i = 1; i < uniqueDates.length; i++) { + const gap = daysBetween(uniqueDates[i - 1], uniqueDates[i]); + if (gap === 1) { + tempStreak++; + longestStreak = Math.max(longestStreak, tempStreak); + } else { + tempStreak = 1; + } + } + longestStreak = Math.max(longestStreak, currentStreak); + + const data: StreakData = { + currentStreak, + longestStreak, + lastLoggedDate: lastDate, + streakDates: uniqueDates.slice(-90), + }; + + saveStreakData(data); + return data; +} + +/** Check if mood was already logged today */ +export function hasLoggedToday(): boolean { + const data = loadStreakData(); + return data.lastLoggedDate === getLocalDateString(); +} + +/** Get the current local date string for external use */ +export function getTodayString(): string { + return getLocalDateString(); +} diff --git a/package-lock.json b/package-lock.json index 0376c41..41f4880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1622,6 +1622,23 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT", + "optional": true + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1661,99 +1678,36 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@floating-ui/core": { @@ -1803,26 +1757,19 @@ "react-hook-form": "^7.0.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=18.18.0" + "node": ">=10.10.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1838,18 +1785,12 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -4056,7 +3997,8 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/glob": { "version": "7.2.0", @@ -4193,7 +4135,7 @@ "debug": "^4.4.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4217,12 +4159,10 @@ "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/scope-manager": { @@ -4235,7 +4175,7 @@ "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4288,7 +4228,7 @@ "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4312,14 +4252,16 @@ "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -4332,9 +4274,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4379,7 +4321,7 @@ "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -6225,62 +6167,58 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", + "cross-spawn": "^7.0.2", "debug": "^4.3.2", + "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-config-next": { @@ -6541,16 +6479,16 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6568,42 +6506,27 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", + "node_modules/eslint/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "eslint-visitor-keys": "^3.4.1" }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6744,15 +6667,15 @@ } }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=16.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/filelist": { @@ -6831,16 +6754,54 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.4" + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=16" + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/flatted": { @@ -6895,24 +6856,21 @@ } }, "node_modules/framer-motion": { - "version": "12.23.12", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", - "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.18.0.tgz", + "integrity": "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==", "license": "MIT", "dependencies": { - "motion-dom": "^12.23.12", - "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" }, "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, "react": { "optional": true }, @@ -7106,12 +7064,27 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7180,6 +7153,12 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8300,21 +8279,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/motion-dom": { - "version": "12.23.12", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", - "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.23.6" - } - }, - "node_modules/motion-utils": { - "version": "12.23.6", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", - "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10467,6 +10431,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "license": "MIT" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -10563,15 +10533,15 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "license": "MIT", "engines": { - "node": ">=18.12" + "node": ">=16" }, "peerDependencies": { - "typescript": ">=4.8.4" + "typescript": ">=4.2.0" } }, "node_modules/ts-interface-checker": { diff --git a/package.json b/package.json index 424cf5c..cb3b865 100644 --- a/package.json +++ b/package.json @@ -84,4 +84,4 @@ "postcss": "^8.5.6", "tailwindcss": "^3.4.0" } -} +} \ No newline at end of file