diff --git a/public/encouragements.json b/public/encouragements.json new file mode 100644 index 0000000..3996d9e --- /dev/null +++ b/public/encouragements.json @@ -0,0 +1,102 @@ +[ + "Great job! Keep going!", + "You're on fire! 🔥", + "Consistency is key!", + "Amazing streak!", + "You crushed it today!", + "Small steps, big results!", + "Habit hero!", + "Progress, not perfection!", + "Every dot counts!", + "Keep up the momentum!", + "You’re building something awesome!", + "One step closer to your goal!", + "You’re unstoppable!", + "Keep the streak alive!", + "You’re making it happen!", + "Your effort is inspiring!", + "You’re a streak superstar!", + "Every day matters!", + "You’re a habit legend!", + "You’re doing fantastic!", + "Keep shining!", + "You’re a role model!", + "You’re a champion!", + "You’re making progress!", + "You’re a winner!", + "You’re a streak master!", + "You’re a habit machine!", + "You’re a streak builder!", + "You’re a streak star!", + "You’re a streak hero!", + "You’re a streak ninja!", + "You’re a streak wizard!", + "You’re a streak warrior!", + "You’re a streak explorer!", + "You’re a streak adventurer!", + "You’re a streak conqueror!", + "You’re a streak champion!", + "You’re a streak genius!", + "You’re a streak guru!", + "You’re a streak expert!", + "You’re a streak pro!", + "You’re a streak veteran!", + "You’re a streak rookie!", + "You’re a streak all-star!", + "You’re a streak MVP!", + "You’re a streak superstar!", + "You’re a streak rockstar!", + "You’re a streak dynamo!", + "You’re a streak powerhouse!", + "You’re a streak inspiration!", + "You’re a streak motivator!", + "You’re a streak leader!", + "You’re a streak innovator!", + "You’re a streak creator!", + "You’re a streak builder!", + "You’re a streak achiever!", + "You’re a streak doer!", + "You’re a streak finisher!", + "You’re a streak starter!", + "You’re a streak closer!", + "You’re a streak winner!", + "You’re a streak believer!", + "You’re a streak dreamer!", + "You’re a streak thinker!", + "You’re a streak planner!", + "You’re a streak organizer!", + "You’re a streak strategist!", + "You’re a streak tactician!", + "You’re a streak visionary!", + "You’re a streak optimist!", + "You’re a streak realist!", + "You’re a streak enthusiast!", + "You’re a streak supporter!", + "You’re a streak encourager!", + "You’re a streak helper!", + "You’re a streak friend!", + "You’re a streak teammate!", + "You’re a streak partner!", + "You’re a streak ally!", + "You’re a streak companion!", + "You’re a streak buddy!", + "You’re a streak pal!", + "You’re a streak mate!", + "You’re a streak peer!", + "You’re a streak colleague!", + "You’re a streak associate!", + "You’re a streak collaborator!", + "You’re a streak contributor!", + "You’re a streak participant!", + "You’re a streak member!", + "You’re a streak player!", + "You’re a streak contender!", + "You’re a streak competitor!", + "You’re a streak challenger!", + "You’re a streak rival!", + "You’re a streak victor!", + "You’re a streak survivor!", + "You’re a streak thriver!", + "You’re a streak overcomer!", + "You’re a streak achiever!" +] diff --git a/src/components/MiniGrid.jsx b/src/components/MiniGrid.jsx index 2b3d50a..07dfcac 100644 --- a/src/components/MiniGrid.jsx +++ b/src/components/MiniGrid.jsx @@ -17,6 +17,7 @@ import { motion } from 'framer-motion'; import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit'; import { getFrozenDays } from '../lib/utils-habit'; import { toggleCompletion } from '../lib/storage'; +import { toast } from './ui/use-toast'; const MiniGrid = ({ habit, onUpdate }) => { const today = new Date(); @@ -39,10 +40,33 @@ const MiniGrid = ({ habit, onUpdate }) => { days.push(date); } - const handleCellClick = (e, date) => { + const handleCellClick = async (e, date) => { e.stopPropagation(); - toggleCompletion(habit.id, formatDate(date)); + const dateStr = formatDate(date); + const isTodayCell = isToday(date); + const wasCompleted = habit.completions.includes(dateStr); + toggleCompletion(habit.id, dateStr); onUpdate(); + // Only show encouragement toast if validating (adding) today's dot + if (isTodayCell && !wasCompleted) { + try { + const res = await fetch('/encouragements.json'); + const messages = await res.json(); + const msg = messages[Math.floor(Math.random() * messages.length)]; + toast({ + title: '🎉 Keep Going!', + description: msg, + duration: 2500, + }); + } catch (err) { + // fallback message + toast({ + title: '🎉 Keep Going!', + description: 'Great job! Keep up the streak!', + duration: 2500, + }); + } + } }; return ( diff --git a/src/components/ui/toast.jsx b/src/components/ui/toast.jsx index c1b11d2..535e5fe 100644 --- a/src/components/ui/toast.jsx +++ b/src/components/ui/toast.jsx @@ -7,19 +7,21 @@ import React from 'react'; const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef(({ className, ...props }, ref) => ( - + )); ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( - 'data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full', + 'data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-2xl border-0 p-6 pr-8 shadow-2xl transition-all bg-white/80 backdrop-blur-lg ring-2 ring-green-300/40 drop-shadow-xl scale-95 animate-toast-in data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full', { variants: { variant: { @@ -73,20 +75,24 @@ const ToastClose = React.forwardRef(({ className, ...props }, ref) => ( ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef(({ className, ...props }, ref) => ( - + + 🎊 {props.children} + )); ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef(({ className, ...props }, ref) => ( - + + ✨ {props.children} + )); ToastDescription.displayName = ToastPrimitives.Description.displayName; diff --git a/src/index.css b/src/index.css index c69a07c..3ef95e6 100644 --- a/src/index.css +++ b/src/index.css @@ -94,4 +94,21 @@ .dark .grid-scroll::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.2); +} + +/* Toast custom animations */ +@keyframes toast-in { + 0% { transform: scale(0.7) translateY(40px); opacity: 0; } + 100% { transform: scale(1) translateY(0); opacity: 1; } +} +.animate-toast-in { + animation: toast-in 0.5s cubic-bezier(.68,-0.55,.27,1.55); +} +@keyframes float { + 0% { transform: translateY(0); } + 50% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} +.animate-float { + animation: float 2s infinite ease-in-out; } \ No newline at end of file