diff --git a/client/package-lock.json b/client/package-lock.json index eab9cb3..5ca7d3f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,6 +18,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^16.4.5", + "framer-motion": "^11.15.0", "lodash": "^4.17.21", "lucide-react": "^0.456.0", "react": "^18.2.0", @@ -9429,6 +9430,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", + "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.14.3", + "motion-utils": "^11.14.3", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -12786,6 +12814,18 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", + "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==", + "license": "MIT" + }, + "node_modules/motion-utils": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", + "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/client/package.json b/client/package.json index 0126662..1dc961a 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^16.4.5", + "framer-motion": "^11.15.0", "lodash": "^4.17.21", "lucide-react": "^0.456.0", "react": "^18.2.0", diff --git a/client/src/components/Streak Management/StreakTracker.js b/client/src/components/Streak Management/StreakTracker.js index a0fbf83..5595d42 100644 --- a/client/src/components/Streak Management/StreakTracker.js +++ b/client/src/components/Streak Management/StreakTracker.js @@ -1,12 +1,27 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import Dashboard from '../Analytics/Dashboard'; import { ChartBarIcon } from '@heroicons/react/24/outline'; import { Bot } from 'lucide-react'; import Chat from '../AI/Chat'; +import { createFireElement } from '../../utils/other/animationsUtils'; const StreakTracker = ({ completedTasks, streakData }) => { const [openDashboard, setOpenDashboard] = React.useState(null); - const [isChatOpen, setIsChatOpen] = React.useState(false); + const [isChatOpen, setIsChatOpen] = useState(false); + const [showFireAnimation, setShowFireAnimation] = useState(false); + const [prevStreak, setPrevStreak] = useState(streakData.current); + + useEffect(() => { + if (streakData.current > prevStreak) { + setShowFireAnimation(true); + const fireTimer = setTimeout(() => { + setShowFireAnimation(false); + }, 2000); + return () => clearTimeout(fireTimer); + } + setPrevStreak(streakData.current); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [prevStreak, streakData?.current]); const handleOpenDashboard = useCallback((opener) => { setOpenDashboard(() => opener); @@ -15,13 +30,19 @@ const StreakTracker = ({ completedTasks, streakData }) => { return (
-
+

Current Streak

-

{streakData.current}

+
+

{streakData.current}

+ {createFireElement(showFireAnimation)} +
-
+

Longest Streak

-

{streakData.longest}

+
+

{streakData.longest}

+ {createFireElement(showFireAnimation && streakData.current >= streakData.longest)} +
diff --git a/client/src/utils/other/animationsUtils.js b/client/src/utils/other/animationsUtils.js new file mode 100644 index 0000000..bb1c12f --- /dev/null +++ b/client/src/utils/other/animationsUtils.js @@ -0,0 +1,36 @@ +import { motion, AnimatePresence } from 'framer-motion'; +import React from 'react'; + +/** + * Creates an animated fire emoji element for streak celebrations + * @param {boolean} isVisible - Controls the visibility of the fire animation + * @returns {JSX.Element|null} A motion-animated fire emoji that scales and fades + */ +export const createFireElement = (isVisible) => { + return ( + + {isVisible && ( + + + 🔥 + + + )} + + ); +};