From abb02da751e3c43cf881157b6115d5a19e4496e6 Mon Sep 17 00:00:00 2001 From: Pranav Ramesh Date: Wed, 13 Nov 2024 17:31:37 -0500 Subject: [PATCH] feat: time tracking --- src/components/StandardNav.jsx | 36 +++++++++++++ src/components/Tooltip.jsx | 16 ++++++ src/pages/dashboard.jsx | 98 ++++++++++++++++++++++++++++++++-- src/utils/timeTracker.js | 81 ++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 src/components/Tooltip.jsx create mode 100644 src/utils/timeTracker.js diff --git a/src/components/StandardNav.jsx b/src/components/StandardNav.jsx index e9293e37..3b7f37e5 100644 --- a/src/components/StandardNav.jsx +++ b/src/components/StandardNav.jsx @@ -38,6 +38,7 @@ import SearchModal from './nav/SearchModal'; import SpawnTerminal from './nav/SpawnTerminal'; import { Context } from '@/context'; import { useContext } from 'react'; +import timeTracker from '@/utils/timeTracker'; function classNames(...classes) { return classes.filter(Boolean).join(' '); @@ -233,6 +234,41 @@ export function StandardNav({ guestAllowed, alignCenter = true }) { }; }, [panelRef]); + // Add time tracking effect at the top level + const timeTrackerInitialized = useRef(false); + + useEffect(() => { + // Only initialize once and only if user is logged in + if (!timeTrackerInitialized.current && !guestAllowed) { + console.log('Initializing time tracker'); + timeTracker.startTracking(); + timeTrackerInitialized.current = true; + + // Cleanup function + return () => { + console.log('Cleaning up time tracker'); + timeTracker.stopTracking(); + }; + } + }, [guestAllowed]); + + useEffect(() => { + const handleVisibilityChange = () => { + if (document.hidden) { + console.log('Page hidden - syncing time'); + timeTracker.syncWithServer(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + // Sync when component unmounts + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + timeTracker.syncWithServer(); + }; + }, []); + return ( <> {isPopoverOpen && ( diff --git a/src/components/Tooltip.jsx b/src/components/Tooltip.jsx new file mode 100644 index 00000000..ee5a4514 --- /dev/null +++ b/src/components/Tooltip.jsx @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +export default function Tooltip({ children }) { + const [isVisible, setIsVisible] = useState(false); + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children[0]} + {isVisible && children[1]} +
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard.jsx b/src/pages/dashboard.jsx index fbfa3b6e..c0038c30 100644 --- a/src/pages/dashboard.jsx +++ b/src/pages/dashboard.jsx @@ -4,7 +4,7 @@ import { StandardNav } from '@/components/StandardNav'; import { DashboardHeader } from '@/components/dashboard/DashboardHeader'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useEffect, useState } from 'react'; -import { ArrowLeftIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/solid'; +import { ArrowLeftIcon, CheckCircleIcon, XCircleIcon, ClockIcon } from '@heroicons/react/24/solid'; import request from '@/utils/request'; import ChallengeCard from '@/components/profile/ChallengeCard'; import { BoltIcon, RocketLaunchIcon, TrophyIcon } from '@heroicons/react/20/solid'; @@ -16,6 +16,8 @@ import { Dialog, Transition } from '@headlessui/react' import { Fragment } from 'react' import OnboardingModal from '@/components/modals/OnboardingModal'; import { UserCircleIcon } from '@heroicons/react/24/solid'; +import timeTracker from '@/utils/timeTracker'; +import Tooltip from '@/components/Tooltip'; const includedFeatures = [ 'Priority machine access', @@ -49,6 +51,7 @@ export default function Dashboard() { ] const [isOnboardingOpen, setIsOnboardingOpen] = useState(false); const [completedTasks, setCompletedTasks] = useState(null); + const [timeProgress, setTimeProgress] = useState(null); useEffect(() => { const user = localStorage.getItem('username'); @@ -200,6 +203,20 @@ export default function Dashboard() { // The API endpoint will handle updating the database }; + useEffect(() => { + const checkTimeProgress = async () => { + const progress = await timeTracker.getWeeklyProgress(); + if (progress) { + setTimeProgress(progress); + } + }; + + checkTimeProgress(); + const interval = setInterval(checkTimeProgress, 300000); + + return () => clearInterval(interval); + }, []); + return ( <> @@ -220,10 +237,11 @@ export default function Dashboard() {
-
+
{completedTasks && (!completedTasks.profilePicture || !completedTasks.firstChallenge) && ( -
+
+

Onboarding Tasks

+
)}
+ {/* Add after the onboarding tasks section */} +{timeProgress && ( +
+

+ Weekly Progress + +
+ beta +
+
+
+

This is an experimental feature.

+ +
+
+
+

+ +
+ {/* Progress Bar */} +
+
+
+ + {/* Stats */} +
+
+

Time Spent

+

+ {Math.round(timeProgress.totalMinutesSpent / 60)}h {timeProgress.totalMinutesSpent % 60}m +

+
+ +
+

Weekly Goal

+

+ {Math.round(timeProgress.weeklyGoalMinutes / 60)}h +

+
+ +
+

Days Left

+

+ {timeProgress.daysLeft} days +

+
+
+ + {/* Status Message */} +
+

+ {timeProgress.onTrack + ? `You're on track! ${Math.round(timeProgress.progressPercentage)}% of your weekly goal complete.` + : `You're behind schedule. ${Math.round(timeProgress.progressPercentage)}% complete with ${timeProgress.daysLeft} days left.` + } +

+
+
+
+)} +

Recommended Challenges

{loading ? <> : ( diff --git a/src/utils/timeTracker.js b/src/utils/timeTracker.js new file mode 100644 index 00000000..6772830b --- /dev/null +++ b/src/utils/timeTracker.js @@ -0,0 +1,81 @@ +import request from './request'; + +class TimeTracker { + constructor() { + this.startTime = null; + this.accumulatedTime = 0; + this.isTracking = false; + this.syncInterval = null; + } + + async getWeeklyProgress() { + try { + const response = await request( + `${process.env.NEXT_PUBLIC_API_URL}/account/weeklyTimeProgress`, + 'GET' + ); + + console.log('Weekly progress response:', response); + return response; + } catch (error) { + console.error('Failed to get weekly progress:', error); + return null; + } + } + + startTracking() { + if (this.isTracking) return; + + console.log('Starting time tracking'); + this.startTime = Date.now(); + this.isTracking = true; + + // Sync every 5 minutes + this.syncInterval = setInterval(() => { + this.syncWithServer(); + }, 5 * 60 * 1000); + } + + stopTracking() { + if (!this.isTracking) return; + + console.log('Stopping time tracking'); + this.syncWithServer(); // Final sync + this.isTracking = false; + if (this.syncInterval) { + clearInterval(this.syncInterval); + this.syncInterval = null; + } + } + + async syncWithServer() { + if (!this.startTime) return; + + const currentTime = Date.now(); + const timeSpent = (currentTime - this.startTime) / 1000 / 60; // Convert to minutes + const minutesSpent = Math.round(timeSpent); + + console.log('Syncing time:', minutesSpent, 'minutes'); + + if (minutesSpent > 0) { + try { + const response = await request( + `${process.env.NEXT_PUBLIC_API_URL}/account/trackTime`, + 'POST', + { + minutesSpent, + date: new Date().toISOString() + } + ); + + console.log('Time tracking response:', response); + this.startTime = currentTime; // Reset start time after successful sync + } catch (error) { + console.error('Failed to sync time:', error); + } + } + } +} + +const timeTracker = new TimeTracker(); +export default timeTracker; \ No newline at end of file