From e104cc51b99348e0b59dc86581ff1e2048af4950 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Mon, 18 Aug 2025 17:32:04 +0530 Subject: [PATCH 01/13] Updated schema's for productivity zone --- backend/src/db/schema.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 18325d0..0b6990d 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -1,4 +1,4 @@ -import { timestamp, pgTable, varchar, integer, unique, pgEnum } from "drizzle-orm/pg-core"; +import { timestamp, pgTable, varchar, integer, unique, pgEnum, date, serial, uuid } from "drizzle-orm/pg-core"; // export const statusEnum = pgEnum("status", ["pending", "completed"]); @@ -33,4 +33,23 @@ export const tasks = pgTable("tasks", { title: varchar("title", { length: 255 }).notNull(), status: statusEnum(), order: integer("order").notNull().default(0) +}); + +export const productivity = pgTable("productivity", { + userId: varchar("user_id").references(() => users.id, {onDelete: 'cascade'}).notNull(), + day: date("day").notNull(), + totalSeconds: integer("total_seconds").notNull().default(0), + sessionsCount: integer("sessions_count").notNull().default(0), + goalSeconds: integer("goal_seconds").notNull().default(0), +}, (t) => [ + unique().on(t.userId, t.day) +]); + +export const productivityAdd = pgTable("productivity_add", { + id: serial("id").primaryKey(), + requestId: uuid("request_id").notNull(), + userId: varchar("user_id").references(() => users.id, {onDelete: 'cascade'}).notNull(), + day: date("day").notNull(), + deltaSeconds: integer("delta_seconds").notNull(), + deltaSessions: integer("delta_sessions").notNull() }); \ No newline at end of file From 825416327ef03813d7298e496151bb180a38ff29 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Thu, 21 Aug 2025 01:10:17 +0530 Subject: [PATCH 02/13] Added code for handling 2 types of timer (work and rest) - basics --- .../components/buttons.tsx | 44 +++++++ .../components/timer.tsx | 31 +++++ .../productivity_zone_page/productivity.tsx | 109 ++++++++++++++++++ .../main_screens/study_page/study.tsx | 3 - frontend/src/islands/ClientDashboard.tsx | 6 +- frontend/src/islands/Sidebar.tsx | 11 +- 6 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx create mode 100644 frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx create mode 100644 frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx delete mode 100644 frontend/src/components/dashboard/main_screens/study_page/study.tsx diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx new file mode 100644 index 0000000..19d0a01 --- /dev/null +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx @@ -0,0 +1,44 @@ +import { Button } from "@/components/ui/button"; +import { Timer } from "lucide-react"; + +interface PropTypes { + handleStart: () => void; + active: boolean; + paused: boolean; + handleStopPause: (type: String) => void; // accepted inputs -> "pause" / "stop" +} + +export default function TimerButtons({ + handleStart, + active, + paused, + handleStopPause +}: PropTypes) { + + return ( +
+ {!active && ( + + )} + {active && ( +
+ + {paused ? ( + + ) : ( + + )} +
+ )} +
+ ); +} diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx new file mode 100644 index 0000000..cc6c124 --- /dev/null +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx @@ -0,0 +1,31 @@ +import { useMemo } from "react"; +import { TimerTypes } from "../productivity"; + +type TimerProps = { + workTime: number; + breakTime: number; + timerType: TimerTypes; +}; // time in seconds + +export default function Timer({ workTime, breakTime, timerType }: TimerProps) { + const display = useMemo(() => { + const t = Math.max(0, Math.floor(timerType === TimerTypes.WORK ? workTime : breakTime)); + const minutes = Math.floor((t % 3600) / 60); + const seconds = t % 60; + + const mm = String(minutes).padStart(2, "0"); + const ss = String(seconds).padStart(2, "0"); + + return ( +

+ {mm}:{ss} +

+ ); + }, [workTime, breakTime, timerType]); + + return ( +
+ {display} +
+ ); +} diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx new file mode 100644 index 0000000..4138158 --- /dev/null +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -0,0 +1,109 @@ +import { useState, useRef, useEffect } from "react"; +import Timer from "./components/timer"; +import TimerButtons from "./components/buttons"; + +export enum TimerTypes { + WORK = "work", + BREAK = "break", +} + +export default function Productivity() { + const [workTime, setWorkTime] = useState(1797); + const [breakTime, setBreakTime] = useState(0); + const [timerType, setTimerType] = useState(TimerTypes.WORK); + const [active, setActive] = useState(false); + const [paused, setPaused] = useState(false); + + const intervalRef = useRef(null); + + useEffect(() => { + if ((workTime % 3600) / 60 === 30 || workTime === 30 * 60) { + // 30 minutes -> Maximum productivity time + setTimerType(TimerTypes.BREAK); + setWorkTime(0); + + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + + intervalRef.current = setInterval(() => { + setBreakTime((prev) => prev + 1); + }, 1000); + // Add a backend call to store the data + } + + if ((workTime % 3600) / 60 === 5 || breakTime === 5 * 60) { + setTimerType(TimerTypes.WORK); + setBreakTime(0); + + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + + intervalRef.current = setInterval(() => { + setWorkTime((prev) => prev + 1); + }, 1000); + } + }, [workTime, breakTime]); + + const handleStartTimer = () => { + setActive(true); + setPaused(false); + + if (intervalRef.current) return; + + if (timerType === TimerTypes.WORK) { + intervalRef.current = setInterval(() => { + setWorkTime((prev) => prev + 1); + }, 1000); + } else { + intervalRef.current = setInterval(() => { + setBreakTime((prev) => prev + 1); + }, 1000); + } + }; + + const handleStopPauseTimer = (type: String) => { + if (type === "stop") { + setActive(false); + setPaused(false); + setWorkTime(0); + setBreakTime(0); + setTimerType(TimerTypes.WORK); + } + if (type === "pause") { + setActive(true); + setPaused(true); + } + if (!intervalRef.current) return; + + clearInterval(intervalRef.current); + + intervalRef.current = null; + }; + + return ( +
+
+

Productivity Zone

+

+ Start studying and working with POMODORO technique and boost your + productivity +

+
+
+ + +
+
+ ); +} diff --git a/frontend/src/components/dashboard/main_screens/study_page/study.tsx b/frontend/src/components/dashboard/main_screens/study_page/study.tsx deleted file mode 100644 index 76e4450..0000000 --- a/frontend/src/components/dashboard/main_screens/study_page/study.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Study () { - return

Study

-} \ No newline at end of file diff --git a/frontend/src/islands/ClientDashboard.tsx b/frontend/src/islands/ClientDashboard.tsx index 60ad844..b4df967 100644 --- a/frontend/src/islands/ClientDashboard.tsx +++ b/frontend/src/islands/ClientDashboard.tsx @@ -1,10 +1,10 @@ import ClerkProviderWrapper from '@/components/ClerkProviderWrapper'; import Home from '@/components/dashboard/main_screens/home_page/home'; import Analytics from '@/components/dashboard/main_screens/analytics_page/analytics'; -import Study from '@/components/dashboard/main_screens/study_page/study'; import Settings from '@/components/dashboard/main_screens/settings_page/settings'; import Sidebar from './Sidebar'; import { useState } from 'react'; +import Productivity from '@/components/dashboard/main_screens/productivity_zone_page/productivity'; const ClientDashboard = () => { @@ -17,12 +17,12 @@ const ClientDashboard = () => { style={{ gridTemplateColumns: collapsed ? "4rem 1fr": "16rem 1fr"}} >
{activeScreen === "home" && } {activeScreen === "analytics" && } - {activeScreen === "study" && } + {activeScreen === "productivity" && } {activeScreen === "settings" && }
diff --git a/frontend/src/islands/Sidebar.tsx b/frontend/src/islands/Sidebar.tsx index cc824a4..3493de5 100644 --- a/frontend/src/islands/Sidebar.tsx +++ b/frontend/src/islands/Sidebar.tsx @@ -14,12 +14,14 @@ import type { Dispatch, SetStateAction } from "react"; import clsx from "clsx"; interface PropTypes { + activeScreen: String; setActiveScreen: Dispatch>; collapsed: boolean; setCollapsed: Dispatch>; } export default function Sidebar({ + activeScreen, setActiveScreen, collapsed, setCollapsed, @@ -40,9 +42,9 @@ export default function Sidebar({ click: "analytics", }, { - name: "Study Zone", + name: "Productivity Zone", icon: , - click: "study", + click: "productivity", }, { name: "Settings", @@ -119,8 +121,9 @@ export default function Sidebar({ className={clsx( "mb-3 text-sm flex items-center p-2 gap-4 cursor-pointer", collapsed - ? "justify-center rounded-full" - : "rounded-lg hover:bg-violet-300 hover:shadow-sm transition-colors" + ? "justify-center rounded-lg py-2 px-4" + : "rounded-lg hover:bg-violet-300 hover:shadow-sm transition-colors", + activeScreen === option.click ? "bg-violet-300" : "" )} onClick={() => setActiveScreen(option.click)} title={collapsed ? option.name : undefined} From 809deadc4d2ac748ea319d20de1eba6d51b253de Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Thu, 21 Aug 2025 02:05:42 +0530 Subject: [PATCH 03/13] Added notifications for taking a break, and starting work, randomized messages, basic set up for total session iterations --- .../home_page/components/CreateList.tsx | 2 +- .../home_page/components/ManageCategories.tsx | 2 +- .../components/SelectCategoryDropdown.tsx | 2 +- .../productivity_zone_page/productivity.tsx | 22 +++++++++++- frontend/src/helpers/capitalizeFirst.ts | 4 --- frontend/src/helpers/helpers.ts | 34 +++++++++++++++++++ 6 files changed, 58 insertions(+), 8 deletions(-) delete mode 100644 frontend/src/helpers/capitalizeFirst.ts create mode 100644 frontend/src/helpers/helpers.ts diff --git a/frontend/src/components/dashboard/main_screens/home_page/components/CreateList.tsx b/frontend/src/components/dashboard/main_screens/home_page/components/CreateList.tsx index 51b695e..2d12442 100644 --- a/frontend/src/components/dashboard/main_screens/home_page/components/CreateList.tsx +++ b/frontend/src/components/dashboard/main_screens/home_page/components/CreateList.tsx @@ -16,7 +16,7 @@ import { Trash } from "lucide-react"; import { useAuth } from "@clerk/clerk-react"; import { toast } from "sonner"; import { LoaderCircle } from "lucide-react"; -import { capitalizeFirst } from "@/helpers/capitalizeFirst"; +import { capitalizeFirst } from "@/helpers/helpers"; import SelectCategoryDropdown from "./SelectCategoryDropdown"; type PropTypes = { diff --git a/frontend/src/components/dashboard/main_screens/home_page/components/ManageCategories.tsx b/frontend/src/components/dashboard/main_screens/home_page/components/ManageCategories.tsx index bf1bd72..a3b5175 100644 --- a/frontend/src/components/dashboard/main_screens/home_page/components/ManageCategories.tsx +++ b/frontend/src/components/dashboard/main_screens/home_page/components/ManageCategories.tsx @@ -19,7 +19,7 @@ import { useAuth } from "@clerk/clerk-react"; import { toast } from "sonner"; import { Skeleton } from "../../../../ui/skeleton"; import { Button } from "../../../../ui/button"; -import { capitalizeFirst } from "@/helpers/capitalizeFirst"; +import { capitalizeFirst } from "@/helpers/helpers"; type PropTypes = { open: boolean; diff --git a/frontend/src/components/dashboard/main_screens/home_page/components/SelectCategoryDropdown.tsx b/frontend/src/components/dashboard/main_screens/home_page/components/SelectCategoryDropdown.tsx index 005bbd5..a5c891e 100644 --- a/frontend/src/components/dashboard/main_screens/home_page/components/SelectCategoryDropdown.tsx +++ b/frontend/src/components/dashboard/main_screens/home_page/components/SelectCategoryDropdown.tsx @@ -1,5 +1,5 @@ import type { Category } from "@/types/category"; -import { capitalizeFirst } from "@/helpers/capitalizeFirst"; +import { capitalizeFirst } from "@/helpers/helpers"; type PropTypes = { categories: Category[] | undefined; diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx index 4138158..ab9fadd 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from "react"; import Timer from "./components/timer"; import TimerButtons from "./components/buttons"; +import { breakNotifications, workNotifications, createNotification } from "@/helpers/helpers"; export enum TimerTypes { WORK = "work", @@ -9,13 +10,21 @@ export enum TimerTypes { export default function Productivity() { const [workTime, setWorkTime] = useState(1797); - const [breakTime, setBreakTime] = useState(0); + const [breakTime, setBreakTime] = useState(297); const [timerType, setTimerType] = useState(TimerTypes.WORK); const [active, setActive] = useState(false); const [paused, setPaused] = useState(false); + const [notificationGranted, setNotificationGranted] = useState(Notification.permission); + const [sessionIterations, setSessionIterations] = useState(0); const intervalRef = useRef(null); + useEffect(() => { + if(notificationGranted === "default" || notificationGranted === "denied") { + Notification.requestPermission().then((permission) => {setNotificationGranted(permission)}) + } + }, []) + useEffect(() => { if ((workTime % 3600) / 60 === 30 || workTime === 30 * 60) { // 30 minutes -> Maximum productivity time @@ -29,6 +38,11 @@ export default function Productivity() { intervalRef.current = setInterval(() => { setBreakTime((prev) => prev + 1); }, 1000); + + const body = breakNotifications[Math.floor(Math.random()*breakNotifications.length)]; + createNotification(body); + + setSessionIterations(prev => prev+0.5); // Add a backend call to store the data } @@ -43,6 +57,11 @@ export default function Productivity() { intervalRef.current = setInterval(() => { setWorkTime((prev) => prev + 1); }, 1000); + + const body = workNotifications[Math.floor(Math.random()*workNotifications.length)]; + createNotification(body); + + setSessionIterations(prev => prev+0.5); } }, [workTime, breakTime]); @@ -103,6 +122,7 @@ export default function Productivity() { paused={paused} handleStopPause={handleStopPauseTimer} /> + {Math.floor(sessionIterations)} ); diff --git a/frontend/src/helpers/capitalizeFirst.ts b/frontend/src/helpers/capitalizeFirst.ts deleted file mode 100644 index b5cd382..0000000 --- a/frontend/src/helpers/capitalizeFirst.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function capitalizeFirst(v: string) { - const t = v.charAt(0).toUpperCase() + v.slice(1); - return t; -} \ No newline at end of file diff --git a/frontend/src/helpers/helpers.ts b/frontend/src/helpers/helpers.ts new file mode 100644 index 0000000..fe1abb1 --- /dev/null +++ b/frontend/src/helpers/helpers.ts @@ -0,0 +1,34 @@ +export function capitalizeFirst(v: string) { + const t = v.charAt(0).toUpperCase() + v.slice(1); + return t; +} + +export function createNotification(body: string) { + return new Notification("TaskAI", { body: body }); +} + +export const breakNotifications = [ + "🎉 Great job! You've completed 30 minutes of focused work. Time to take a short break!", + "✅ 30 minutes done! Step away for a quick break and recharge.", + "⏳ Half an hour of productivity completed! Treat yourself to a short break.", + "👏 Well done! You’ve worked for 30 minutes straight. Take 5 minutes to relax.", + "⚡ Awesome focus! 30 minutes logged — time for a quick stretch.", + "🌿 30 minutes of work completed! Pause for a short refresh.", + "💡 You’ve been working for 30 minutes. A short break will keep your energy up!", + "⏰ Break time! 30 minutes of solid work achieved.", + "✨ Focus session complete — 30 minutes done! Take a short break before resuming.", + "🚀 Productivity boost: You’ve worked 30 minutes nonstop. Now, recharge with a break." +]; + +export const workNotifications = [ + "💪 Break’s over! Let’s get back to another 30 minutes of focused work.", + "🚀 Ready to dive in? Start your next 30-minute session now!", + "⏰ Time to get back on track. Begin your next focus block.", + "✨ Refreshed? Let’s crush the next 30 minutes of work!", + "🔥 Break complete! Jump back into your workflow.", + "📚 Time to focus again. Start your next productive session!", + "🌟 Let’s build momentum — your next 30 minutes of work starts now.", + "✅ Recharged and ready? Continue with your next focus session.", + "⚡ Back to work mode! Another 30-minute sprint begins.", + "👏 You’re doing great! Time to start the next work session." +]; From 97abea196d38b676db80b1fd47a535f8a28089c3 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Fri, 22 Aug 2025 12:54:24 +0530 Subject: [PATCH 04/13] Updated productivity zone, fixed bugs --- .../components/buttons.tsx | 2 +- .../components/timer.tsx | 2 +- .../productivity_zone_page/productivity.tsx | 140 +++++++++++------- 3 files changed, 92 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx index 19d0a01..6c01d53 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx @@ -5,7 +5,7 @@ interface PropTypes { handleStart: () => void; active: boolean; paused: boolean; - handleStopPause: (type: String) => void; // accepted inputs -> "pause" / "stop" + handleStopPause: (type: string) => void; // accepted inputs -> "pause" / "stop" } export default function TimerButtons({ diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx index cc6c124..34b73d5 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx @@ -17,7 +17,7 @@ export default function Timer({ workTime, breakTime, timerType }: TimerProps) { const ss = String(seconds).padStart(2, "0"); return ( -

+

{mm}:{ss}

); diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx index ab9fadd..fc7ee17 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useMemo } from "react"; import Timer from "./components/timer"; import TimerButtons from "./components/buttons"; import { breakNotifications, workNotifications, createNotification } from "@/helpers/helpers"; @@ -8,122 +8,162 @@ export enum TimerTypes { BREAK = "break", } +const getNumber = (s: string | null, fallback: number) => { + const n = Number(s); + return Number.isFinite(n) && n >= 0 ? n : fallback; +}; + export default function Productivity() { - const [workTime, setWorkTime] = useState(1797); - const [breakTime, setBreakTime] = useState(297); + const [workTime, setWorkTime] = useState(getNumber(localStorage.getItem("work_time"), 29 * 60)); + const [breakTime, setBreakTime] = useState(getNumber(localStorage.getItem("break_time"), 4 * 60)); + const [timerType, setTimerType] = useState(TimerTypes.WORK); const [active, setActive] = useState(false); const [paused, setPaused] = useState(false); const [notificationGranted, setNotificationGranted] = useState(Notification.permission); const [sessionIterations, setSessionIterations] = useState(0); + const [sessionTotalWorkTime, setSessionTotalWorkTime] = useState(0); - const intervalRef = useRef(null); + const intervalRef = useRef(null); + const clearTick = () => { + if (intervalRef.current !== null) { + window.clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; useEffect(() => { - if(notificationGranted === "default" || notificationGranted === "denied") { - Notification.requestPermission().then((permission) => {setNotificationGranted(permission)}) + localStorage.setItem("work_time", String(workTime)); + }, [workTime]); + useEffect(() => { + localStorage.setItem("break_time", String(breakTime)); + }, [breakTime]); + + // Ask for notifications once + useEffect(() => { + if (notificationGranted === "default" || notificationGranted === "denied") { + Notification.requestPermission().then(setNotificationGranted).catch(() => {}); } - }, []) + }, []); + + // Clean up on unmount + useEffect(() => () => clearTick(), []); + useEffect(() => { + if (!active || paused) return; + + // Work (30:00) -> Break if ((workTime % 3600) / 60 === 30 || workTime === 30 * 60) { - // 30 minutes -> Maximum productivity time setTimerType(TimerTypes.BREAK); setWorkTime(0); + clearTick(); - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - - intervalRef.current = setInterval(() => { + intervalRef.current = window.setInterval(() => { setBreakTime((prev) => prev + 1); }, 1000); - const body = breakNotifications[Math.floor(Math.random()*breakNotifications.length)]; + const body = breakNotifications[Math.floor(Math.random() * breakNotifications.length)]; createNotification(body); - setSessionIterations(prev => prev+0.5); - // Add a backend call to store the data + setSessionIterations((prev) => prev + 0.5); } - if ((workTime % 3600) / 60 === 5 || breakTime === 5 * 60) { + if ((breakTime % 3600) / 60 === 5 || breakTime === 5 * 60) { setTimerType(TimerTypes.WORK); setBreakTime(0); + clearTick(); - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - - intervalRef.current = setInterval(() => { + intervalRef.current = window.setInterval(() => { setWorkTime((prev) => prev + 1); + setSessionTotalWorkTime((prev) => prev + 1); }, 1000); - const body = workNotifications[Math.floor(Math.random()*workNotifications.length)]; + const body = workNotifications[Math.floor(Math.random() * workNotifications.length)]; createNotification(body); - setSessionIterations(prev => prev+0.5); + setSessionIterations((prev) => prev + 0.5); } - }, [workTime, breakTime]); + }, [active, paused, workTime, breakTime]); const handleStartTimer = () => { setActive(true); setPaused(false); - if (intervalRef.current) return; + // If a stale interval exists, clear it so we can start fresh + clearTick(); if (timerType === TimerTypes.WORK) { - intervalRef.current = setInterval(() => { + intervalRef.current = window.setInterval(() => { setWorkTime((prev) => prev + 1); + setSessionTotalWorkTime((prev) => prev + 1); }, 1000); } else { - intervalRef.current = setInterval(() => { + intervalRef.current = window.setInterval(() => { setBreakTime((prev) => prev + 1); }, 1000); } }; - const handleStopPauseTimer = (type: String) => { + const handleStopPauseTimer = (type: string) => { if (type === "stop") { setActive(false); setPaused(false); setWorkTime(0); setBreakTime(0); setTimerType(TimerTypes.WORK); + clearTick(); + return; } + if (type === "pause") { setActive(true); setPaused(true); + clearTick(); // important: allow Start to create a new interval } - if (!intervalRef.current) return; - - clearInterval(intervalRef.current); - - intervalRef.current = null; }; + // session time display + const timeDisplay = useMemo(() => { + const t = Math.max(0, Math.floor(sessionTotalWorkTime)); + const hours = Math.floor(t / 3600); + const minutes = Math.floor((t % 3600) / 60); + const seconds = t % 60; + const hh = String(hours).padStart(2, "0"); + const mm = String(minutes).padStart(2, "0"); + const ss = String(seconds).padStart(2, "0"); + return {hh}:{mm}:{ss}; + }, [sessionTotalWorkTime]); + return (

Productivity Zone

- Start studying and working with POMODORO technique and boost your - productivity + Start studying and working with POMODORO technique and boost your productivity

-
- - - {Math.floor(sessionIterations)} -
+ +
+
+ + +
+ +
+
+ Iterations this session: {Math.floor(sessionIterations)} +
+
+ Time spent in session: {timeDisplay} +
+
+
); } From 547909a1482ecb7e60544693b8197dec849aef8f Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Fri, 22 Aug 2025 14:50:47 +0530 Subject: [PATCH 05/13] Added pomodoro timer, that runs in background --- .../components/buttons.tsx | 2 +- .../components/timer.tsx | 2 +- .../productivity_zone_page/productivity.tsx | 143 ++----------- frontend/src/context/useTimer.tsx | 190 ++++++++++++++++++ frontend/src/helpers/helpers.ts | 5 + frontend/src/islands/ClientDashboard.tsx | 3 + frontend/src/pages/dashboard.astro | 2 +- 7 files changed, 219 insertions(+), 128 deletions(-) create mode 100644 frontend/src/context/useTimer.tsx diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx index 6c01d53..5e8130a 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/buttons.tsx @@ -5,7 +5,7 @@ interface PropTypes { handleStart: () => void; active: boolean; paused: boolean; - handleStopPause: (type: string) => void; // accepted inputs -> "pause" / "stop" + handleStopPause: (type: "stop" | "pause") => void; // accepted inputs -> "pause" / "stop" } export default function TimerButtons({ diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx index 34b73d5..7122ac3 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/components/timer.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { TimerTypes } from "../productivity"; +import { TimerTypes } from "@/helpers/helpers"; type TimerProps = { workTime: number; diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx index fc7ee17..22548d1 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -1,12 +1,8 @@ import { useState, useRef, useEffect, useMemo } from "react"; import Timer from "./components/timer"; import TimerButtons from "./components/buttons"; -import { breakNotifications, workNotifications, createNotification } from "@/helpers/helpers"; +import { useTimer } from "@/context/useTimer"; -export enum TimerTypes { - WORK = "work", - BREAK = "break", -} const getNumber = (s: string | null, fallback: number) => { const n = Number(s); @@ -14,126 +10,23 @@ const getNumber = (s: string | null, fallback: number) => { }; export default function Productivity() { - const [workTime, setWorkTime] = useState(getNumber(localStorage.getItem("work_time"), 29 * 60)); - const [breakTime, setBreakTime] = useState(getNumber(localStorage.getItem("break_time"), 4 * 60)); - - const [timerType, setTimerType] = useState(TimerTypes.WORK); - const [active, setActive] = useState(false); - const [paused, setPaused] = useState(false); - const [notificationGranted, setNotificationGranted] = useState(Notification.permission); - const [sessionIterations, setSessionIterations] = useState(0); - const [sessionTotalWorkTime, setSessionTotalWorkTime] = useState(0); - - const intervalRef = useRef(null); - const clearTick = () => { - if (intervalRef.current !== null) { - window.clearInterval(intervalRef.current); - intervalRef.current = null; - } - }; - - useEffect(() => { - localStorage.setItem("work_time", String(workTime)); - }, [workTime]); - useEffect(() => { - localStorage.setItem("break_time", String(breakTime)); - }, [breakTime]); - - // Ask for notifications once - useEffect(() => { - if (notificationGranted === "default" || notificationGranted === "denied") { - Notification.requestPermission().then(setNotificationGranted).catch(() => {}); - } - }, []); - - // Clean up on unmount - useEffect(() => () => clearTick(), []); - - - useEffect(() => { - if (!active || paused) return; - - // Work (30:00) -> Break - if ((workTime % 3600) / 60 === 30 || workTime === 30 * 60) { - setTimerType(TimerTypes.BREAK); - setWorkTime(0); - clearTick(); - - intervalRef.current = window.setInterval(() => { - setBreakTime((prev) => prev + 1); - }, 1000); - - const body = breakNotifications[Math.floor(Math.random() * breakNotifications.length)]; - createNotification(body); - - setSessionIterations((prev) => prev + 0.5); - } - - if ((breakTime % 3600) / 60 === 5 || breakTime === 5 * 60) { - setTimerType(TimerTypes.WORK); - setBreakTime(0); - clearTick(); - - intervalRef.current = window.setInterval(() => { - setWorkTime((prev) => prev + 1); - setSessionTotalWorkTime((prev) => prev + 1); - }, 1000); - - const body = workNotifications[Math.floor(Math.random() * workNotifications.length)]; - createNotification(body); - - setSessionIterations((prev) => prev + 0.5); - } - }, [active, paused, workTime, breakTime]); - - const handleStartTimer = () => { - setActive(true); - setPaused(false); - - // If a stale interval exists, clear it so we can start fresh - clearTick(); - - if (timerType === TimerTypes.WORK) { - intervalRef.current = window.setInterval(() => { - setWorkTime((prev) => prev + 1); - setSessionTotalWorkTime((prev) => prev + 1); - }, 1000); - } else { - intervalRef.current = window.setInterval(() => { - setBreakTime((prev) => prev + 1); - }, 1000); - } - }; - - const handleStopPauseTimer = (type: string) => { - if (type === "stop") { - setActive(false); - setPaused(false); - setWorkTime(0); - setBreakTime(0); - setTimerType(TimerTypes.WORK); - clearTick(); - return; - } - - if (type === "pause") { - setActive(true); - setPaused(true); - clearTick(); // important: allow Start to create a new interval - } + + const { + status, timerType, workTime, breakTime, + start, pause, stop, + sessionIterations, sessionTotalWorkTime, formatHMS + } = useTimer(); + + const active = status !== "idle"; + const paused = status === "paused"; + + const handleStart = () => start(); + const handleStopPause = (type: "stop" | "pause") => { + if (type === "pause") pause(); + else stop(); }; - // session time display - const timeDisplay = useMemo(() => { - const t = Math.max(0, Math.floor(sessionTotalWorkTime)); - const hours = Math.floor(t / 3600); - const minutes = Math.floor((t % 3600) / 60); - const seconds = t % 60; - const hh = String(hours).padStart(2, "0"); - const mm = String(minutes).padStart(2, "0"); - const ss = String(seconds).padStart(2, "0"); - return {hh}:{mm}:{ss}; - }, [sessionTotalWorkTime]); + const timeDisplay = useMemo(() => formatHMS(sessionTotalWorkTime), [sessionTotalWorkTime]); return (
@@ -148,10 +41,10 @@ export default function Productivity() {
diff --git a/frontend/src/context/useTimer.tsx b/frontend/src/context/useTimer.tsx new file mode 100644 index 0000000..32df69b --- /dev/null +++ b/frontend/src/context/useTimer.tsx @@ -0,0 +1,190 @@ +import React, { + createContext, useContext, useEffect, useMemo, useRef, useState +} from "react"; +import { + TimerTypes, + breakNotifications, + workNotifications, + createNotification, +} from "@/helpers/helpers"; + +type Status = "idle" | "running" | "paused"; + +type TimerContextValue = { + status: Status; + timerType: TimerTypes; + workTime: number; // derived seconds (committed + live) in current WORK block + breakTime: number; // derived seconds in current BREAK block + sessionIterations: number; // +0.5 on each switch + sessionTotalWorkTime: number; // all WORK seconds this session (committed + live) + start: () => void; + pause: () => void; + stop: () => void; + toggle: () => void; + formatHMS: (sec: number) => string; +}; + +const TimerContext = createContext(null); + +const WORK_LIMIT = 30 * 60; // 30:00 +const BREAK_LIMIT = 5 * 60; // 05:00 + +const getNumber = (s: string | null, fallback: number) => { + const n = Number(s); + return Number.isFinite(n) && n >= 0 ? n : fallback; +}; + +const formatHMS = (sec: number) => { + const t = Math.max(0, Math.floor(sec)); + const h = Math.floor(t / 3600); + const m = Math.floor((t % 3600) / 60); + const s = t % 60; + const pad = (x: number) => String(x).padStart(2, "0"); + return `${pad(h)}:${pad(m)}:${pad(s)}`; +}; + +export function TimerProvider({ children }: { children: React.ReactNode }) { + // Committed (accumulated) seconds for the *current* block + const [workAccum, setWorkAccum] = useState(() => getNumber(localStorage.getItem("work_time"), 29*60)); + const [breakAccum, setBreakAccum] = useState(() => getNumber(localStorage.getItem("break_time"), 5*60)); + + // Session aggregates + const [sessionWorkAccum, setSessionWorkAccum] = + useState(() => getNumber(localStorage.getItem("session_total_work"), 0)); + const [sessionIterations, setSessionIterations] = + useState(() => getNumber(localStorage.getItem("session_iterations"), 0)); + + // State + const [status, setStatus] = useState("idle"); + const [timerType, setTimerType] = useState(TimerTypes.WORK); + + // Timestamp for current live segment (null if paused/idle) + const [startedAt, setStartedAt] = useState(null); + + // Persist (optional—remove if you don’t want persistence) + useEffect(() => localStorage.setItem("work_time", String(workAccum)), [workAccum]); + useEffect(() => localStorage.setItem("break_time", String(breakAccum)), [breakAccum]); + useEffect(() => localStorage.setItem("session_total_work", String(sessionWorkAccum)), [sessionWorkAccum]); + useEffect(() => localStorage.setItem("session_iterations", String(sessionIterations)), [sessionIterations]); + + // 1s tick to refresh UI; time itself is derived from Date.now() + const tickRef = useRef(null); + const [, force] = useState(0); + useEffect(() => { + if (status === "running" && !tickRef.current) { + tickRef.current = window.setInterval(() => force(v => v + 1), 1000); + } + if (status !== "running" && tickRef.current) { + clearInterval(tickRef.current); + tickRef.current = null; + } + return () => { + if (tickRef.current) { + clearInterval(tickRef.current); + tickRef.current = null; + } + }; + }, [status]); + + // Live seconds since start of current segment + const liveSec = startedAt && status === "running" + ? Math.floor((Date.now() - startedAt) / 1000) + : 0; + + // Derived block times (committed + live) + const workTime = useMemo( + () => (timerType === TimerTypes.WORK ? workAccum + liveSec : workAccum), + [timerType, workAccum, liveSec] + ); + const breakTime = useMemo( + () => (timerType === TimerTypes.BREAK ? breakAccum + liveSec : breakAccum), + [timerType, breakAccum, liveSec] + ); + + // Session total (include live only during WORK) + const sessionTotalWorkTime = useMemo( + () => sessionWorkAccum + (timerType === TimerTypes.WORK ? liveSec : 0), + [sessionWorkAccum, timerType, liveSec] + ); + + // Auto-switch logic + useEffect(() => { + if (status !== "running") return; + + // WORK -> BREAK + if (timerType === TimerTypes.WORK && workTime >= WORK_LIMIT) { + if (liveSec > 0) setSessionWorkAccum(v => v + liveSec); // commit live to session + setWorkAccum(0); // reset block + setTimerType(TimerTypes.BREAK); + setStartedAt(Date.now()); // start break immediately + setSessionIterations(i => i + 0.5); + createNotification(breakNotifications[Math.floor(Math.random() * breakNotifications.length)]); + } + + // BREAK -> WORK + if (timerType === TimerTypes.BREAK && breakTime >= BREAK_LIMIT) { + setBreakAccum(0); + setTimerType(TimerTypes.WORK); + setStartedAt(Date.now()); // start work immediately + setSessionIterations(i => i + 0.5); + createNotification(workNotifications[Math.floor(Math.random() * workNotifications.length)]); + } + }, [status, timerType, workTime, breakTime, liveSec]); + + // Controls + const start = () => { + if (status === "running") return; + setStatus("running"); + setStartedAt(Date.now()); + }; + + const pause = () => { + if (status !== "running" || !startedAt) { + setStatus("paused"); + return; + } + const sec = Math.floor((Date.now() - startedAt) / 1000); + if (timerType === TimerTypes.WORK) { + setWorkAccum(v => v + sec); + setSessionWorkAccum(v => v + sec); + } else { + setBreakAccum(v => v + sec); + } + setStartedAt(null); + setStatus("paused"); + }; + + const stop = () => { + setStartedAt(null); + setStatus("idle"); + setTimerType(TimerTypes.WORK); + setWorkAccum(0); + setBreakAccum(0); + + // Keep session totals (wipe them here if you prefer) + }; + + const toggle = () => (status === "running" ? pause() : start()); + + const value = useMemo(() => ({ + status, + timerType, + workTime, + breakTime, + sessionIterations, + sessionTotalWorkTime, + start, + pause, + stop, + toggle, + formatHMS, + }), [status, timerType, workTime, breakTime, sessionIterations, sessionTotalWorkTime]); + + return {children}; +} + +export function useTimer() { + const ctx = useContext(TimerContext); + if (!ctx) throw new Error("useTimer must be used within "); + return ctx; +} diff --git a/frontend/src/helpers/helpers.ts b/frontend/src/helpers/helpers.ts index fe1abb1..2ce4213 100644 --- a/frontend/src/helpers/helpers.ts +++ b/frontend/src/helpers/helpers.ts @@ -32,3 +32,8 @@ export const workNotifications = [ "⚡ Back to work mode! Another 30-minute sprint begins.", "👏 You’re doing great! Time to start the next work session." ]; + +export enum TimerTypes { + WORK = "work", + BREAK = "break", +} \ No newline at end of file diff --git a/frontend/src/islands/ClientDashboard.tsx b/frontend/src/islands/ClientDashboard.tsx index b4df967..b028bb9 100644 --- a/frontend/src/islands/ClientDashboard.tsx +++ b/frontend/src/islands/ClientDashboard.tsx @@ -5,6 +5,7 @@ import Settings from '@/components/dashboard/main_screens/settings_page/settings import Sidebar from './Sidebar'; import { useState } from 'react'; import Productivity from '@/components/dashboard/main_screens/productivity_zone_page/productivity'; +import { TimerProvider } from '@/context/useTimer'; const ClientDashboard = () => { @@ -13,6 +14,7 @@ const ClientDashboard = () => { return ( +
@@ -26,6 +28,7 @@ const ClientDashboard = () => { {activeScreen === "settings" && }
+
) } diff --git a/frontend/src/pages/dashboard.astro b/frontend/src/pages/dashboard.astro index ae07959..f436f2b 100644 --- a/frontend/src/pages/dashboard.astro +++ b/frontend/src/pages/dashboard.astro @@ -14,6 +14,6 @@ if (!userId) { description="User dashbaord, create tasklist, edit tasklist, manage your tasks. Use AI to generate your tasks and manage your life." >
- +
From aba75f53031b804b3b675afa493394f769b003c8 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Sat, 23 Aug 2025 19:09:18 +0530 Subject: [PATCH 06/13] Updated session data storage from local to session, fixed method calling --- .../productivity_zone_page/productivity.tsx | 2 +- frontend/src/context/useTimer.tsx | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx index 22548d1..283c8b1 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -41,7 +41,7 @@ export default function Productivity() {
{ export function TimerProvider({ children }: { children: React.ReactNode }) { // Committed (accumulated) seconds for the *current* block - const [workAccum, setWorkAccum] = useState(() => getNumber(localStorage.getItem("work_time"), 29*60)); - const [breakAccum, setBreakAccum] = useState(() => getNumber(localStorage.getItem("break_time"), 5*60)); + const [workAccum, setWorkAccum] = useState(() => getNumber(sessionStorage.getItem("work_time"), 0)); + const [breakAccum, setBreakAccum] = useState(() => getNumber(sessionStorage.getItem("break_time"), 0)); // Session aggregates const [sessionWorkAccum, setSessionWorkAccum] = - useState(() => getNumber(localStorage.getItem("session_total_work"), 0)); + useState(() => getNumber(sessionStorage.getItem("session_total_work"), 0)); const [sessionIterations, setSessionIterations] = - useState(() => getNumber(localStorage.getItem("session_iterations"), 0)); + useState(() => getNumber(sessionStorage.getItem("session_iterations"), 0)); // State const [status, setStatus] = useState("idle"); @@ -62,10 +62,10 @@ export function TimerProvider({ children }: { children: React.ReactNode }) { const [startedAt, setStartedAt] = useState(null); // Persist (optional—remove if you don’t want persistence) - useEffect(() => localStorage.setItem("work_time", String(workAccum)), [workAccum]); - useEffect(() => localStorage.setItem("break_time", String(breakAccum)), [breakAccum]); - useEffect(() => localStorage.setItem("session_total_work", String(sessionWorkAccum)), [sessionWorkAccum]); - useEffect(() => localStorage.setItem("session_iterations", String(sessionIterations)), [sessionIterations]); + useEffect(() => sessionStorage.setItem("work_time", String(workAccum)), [workAccum]); + useEffect(() => sessionStorage.setItem("break_time", String(breakAccum)), [breakAccum]); + useEffect(() => sessionStorage.setItem("session_total_work", String(sessionWorkAccum)), [sessionWorkAccum]); + useEffect(() => sessionStorage.setItem("session_iterations", String(sessionIterations)), [sessionIterations]); // 1s tick to refresh UI; time itself is derived from Date.now() const tickRef = useRef(null); @@ -155,13 +155,20 @@ export function TimerProvider({ children }: { children: React.ReactNode }) { }; const stop = () => { + if(startedAt) { + const sec = Math.floor((Date.now() - startedAt)/1000); + if(timerType === TimerTypes.WORK) { + setSessionWorkAccum(v => v+sec); + setWorkAccum(0); + } else { + setBreakAccum(0); + } + } setStartedAt(null); setStatus("idle"); setTimerType(TimerTypes.WORK); setWorkAccum(0); setBreakAccum(0); - - // Keep session totals (wipe them here if you prefer) }; const toggle = () => (status === "running" ? pause() : start()); From 432a2c9f29c308440f1f307771d49a16cf2b466c Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Sun, 24 Aug 2025 20:47:42 +0530 Subject: [PATCH 07/13] Updated schema for timer, Implemented backend api in useTimer context, created api for adding time in database, and getting totalDuration per date --- backend/src/db/schema.ts | 30 ++---- backend/src/index.ts | 2 + backend/src/routes/productivityRoutes.ts | 112 +++++++++++++++++++++ backend/src/routes/taskRoutes.ts | 2 +- frontend/src/context/useTimer.tsx | 123 ++++++++++++++++------- 5 files changed, 215 insertions(+), 54 deletions(-) create mode 100644 backend/src/routes/productivityRoutes.ts diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 0b6990d..6fe2f83 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -1,4 +1,4 @@ -import { timestamp, pgTable, varchar, integer, unique, pgEnum, date, serial, uuid } from "drizzle-orm/pg-core"; +import { timestamp, pgTable, varchar, integer, unique, pgEnum, serial, date } from "drizzle-orm/pg-core"; // export const statusEnum = pgEnum("status", ["pending", "completed"]); @@ -6,7 +6,7 @@ export const users = pgTable("users", { id: varchar("id", { length: 255 }).primaryKey(), email: varchar("email", { length: 255 }).notNull(), name: varchar("name", { length: 255 }), - createdAt: timestamp("created_at"), + createdAt: timestamp("created_at", { withTimezone: true }), }); export const categories = pgTable("categories", { @@ -22,7 +22,7 @@ export const tasksList = pgTable("tasks_lists", { userId: varchar("user_id").references(() => users.id, {onDelete: 'cascade'}).notNull(), title: varchar("title", { length: 255 }).notNull(), categoryId: integer("category_id").references(() => categories.id, {onDelete: 'set null'}), - createdAt: timestamp("created_at").defaultNow() + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow() }); export const statusEnum = pgEnum("status", ['incomplete', 'in_progress', 'completed']); @@ -35,21 +35,13 @@ export const tasks = pgTable("tasks", { order: integer("order").notNull().default(0) }); -export const productivity = pgTable("productivity", { - userId: varchar("user_id").references(() => users.id, {onDelete: 'cascade'}).notNull(), - day: date("day").notNull(), - totalSeconds: integer("total_seconds").notNull().default(0), - sessionsCount: integer("sessions_count").notNull().default(0), - goalSeconds: integer("goal_seconds").notNull().default(0), -}, (t) => [ - unique().on(t.userId, t.day) -]); - -export const productivityAdd = pgTable("productivity_add", { +export const productivityTimer = pgTable("productivity_timer", { id: serial("id").primaryKey(), - requestId: uuid("request_id").notNull(), - userId: varchar("user_id").references(() => users.id, {onDelete: 'cascade'}).notNull(), - day: date("day").notNull(), - deltaSeconds: integer("delta_seconds").notNull(), - deltaSessions: integer("delta_sessions").notNull() + userId: varchar("user_id").references(() => users.id, { onDelete: 'cascade'}).notNull(), + startedAt: timestamp("started_at", { withTimezone: true }).notNull(), + endedAt: timestamp("ended_at", { withTimezone: true }).notNull(), + duration: integer("duration"), + date: date("date").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow() }); \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index a88ffe6..222f55c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -11,6 +11,7 @@ import { swaggerUI, SwaggerUI } from '@hono/swagger-ui'; import { swaggerSpec } from './lib/swagger'; import analyticsRouter from './routes/analyticsRoutes'; +import pRouter from './routes/productivityRoutes'; const app = new Hono() @@ -55,6 +56,7 @@ app.route("/user", userRouter); app.route("/task", tasksRouter); app.route("/category", categoryRouter); app.route("/analytics", analyticsRouter); +app.route("/prod", pRouter); // pRouter -> productivityRouter (Didn't want to write productivityRouter over and over again) const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_GEMINI_API_KEY }); diff --git a/backend/src/routes/productivityRoutes.ts b/backend/src/routes/productivityRoutes.ts new file mode 100644 index 0000000..b50046c --- /dev/null +++ b/backend/src/routes/productivityRoutes.ts @@ -0,0 +1,112 @@ +import { Hono } from "hono"; +import { requireAuth } from "../middleware/requireAuth"; +import { productivityTimer } from "../db/schema"; +import { db } from "../db/db"; +import { eq, and } from "drizzle-orm"; + +const pRouter = new Hono(); + +type AddBodyType = { + // checked + startTime: number; + endTime: number; +}; + +pRouter.post("/add", requireAuth, async (c) => { + try { + const { userId } = c.get("authData"); + const { startTime, endTime }: AddBodyType = await c.req.json(); + + if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) { + return c.json({ message: "Start and end must be finite!" }, 400); + } + + const durationSec = (endTime - startTime) / 1000; + + const startDate = new Date(startTime); + const endDate = new Date(endTime); + + const dateOnly = startDate.toISOString().split("T")[0]; + + // checking for existing data + const dates = await db + .select() + .from(productivityTimer) + .where( + and( + eq(productivityTimer.date, dateOnly), + eq(productivityTimer.startedAt, startDate), + eq(productivityTimer.endedAt, endDate), + eq(productivityTimer.userId, userId) + ) + ); + console.log("Existing Dates =", dates); + + if (dates.length == 0) { + // If there is no existing entry + const res = await db.insert(productivityTimer).values({ + userId: userId, + startedAt: startDate, + endedAt: endDate, + duration: Math.floor(durationSec), + date: dateOnly, + }); + + console.log("Result = " + res); + + return c.json( + { success: true, message: "Time has been added", res }, + 200 + ); + } else { + console.error("The given time already exists!"); + return c.json({ message: "Already data present" }, 400); + } + } catch (err) { + console.error("An error occured while adding time", err); + return c.json({ message: "Internal Server Error" }, 500); + } +}); + +type Group = { + date: string; + totalDuration: number; +} + +type Row = { + date: string; + duration: number | null; +} + +pRouter.get("/get_time", requireAuth, async (c) => { + try { + const { userId } = c.get("authData"); + const data: Row[] = await db + .select({ + duration: productivityTimer.duration, + date: productivityTimer.date, + }) + .from(productivityTimer) + .where(eq(productivityTimer.userId, userId)); + + console.log("Data is =", data); + + const dataFixed = data.reduce((acc, { date, duration }) => { + + const amt = duration ?? 0; + const found = acc.find((ele) => ele.date === date); + if(found) { + found.totalDuration += amt; + } else { + acc.push({ date, totalDuration: amt }); + } + return acc; + }, []); + return c.json({ dataFixed }, 200); + } catch (err) { + console.error("An error occured while fetching durations per day =", err); + return c.json({ message: "Internal Server Error" }, 500); + } +}); + +export default pRouter; diff --git a/backend/src/routes/taskRoutes.ts b/backend/src/routes/taskRoutes.ts index c64a336..f41e6e4 100644 --- a/backend/src/routes/taskRoutes.ts +++ b/backend/src/routes/taskRoutes.ts @@ -588,7 +588,7 @@ tasksRouter.delete('/delete_task/:id', requireAuth, async (c) => { * content: * application/json: * schema: - * type: object* + * type: object */ tasksRouter.post('/add_list', requireAuth, async (c) => { try { diff --git a/frontend/src/context/useTimer.tsx b/frontend/src/context/useTimer.tsx index 7cbbda3..4f7e965 100644 --- a/frontend/src/context/useTimer.tsx +++ b/frontend/src/context/useTimer.tsx @@ -7,6 +7,8 @@ import { workNotifications, createNotification, } from "@/helpers/helpers"; +import { toast } from "sonner"; +import { useAuth } from "@clerk/clerk-react"; type Status = "idle" | "running" | "paused"; @@ -44,6 +46,10 @@ const formatHMS = (sec: number) => { }; export function TimerProvider({ children }: { children: React.ReactNode }) { + + const { getToken } = useAuth(); + const baseUrl = import.meta.env.PUBLIC_BACKEND_URL; + // Committed (accumulated) seconds for the *current* block const [workAccum, setWorkAccum] = useState(() => getNumber(sessionStorage.getItem("work_time"), 0)); const [breakAccum, setBreakAccum] = useState(() => getNumber(sessionStorage.getItem("break_time"), 0)); @@ -133,43 +139,92 @@ export function TimerProvider({ children }: { children: React.ReactNode }) { // Controls const start = () => { - if (status === "running") return; - setStatus("running"); - setStartedAt(Date.now()); - }; - - const pause = () => { - if (status !== "running" || !startedAt) { - setStatus("paused"); - return; - } - const sec = Math.floor((Date.now() - startedAt) / 1000); + if (status === "running") return; + const now = Date.now(); + setStatus("running"); + setStartedAt(now); +}; + +const pause = async () => { + if (status !== "running" || !startedAt) { + setStatus("paused"); + return; + } + + const end = Date.now(); + + // update local accumulators + const sec = Math.floor((end - startedAt) / 1000); + if (timerType === TimerTypes.WORK) { + setWorkAccum(v => v + sec); + setSessionWorkAccum(v => v + sec); + } else { + setBreakAccum(v => v + sec); + } + + setStatus("paused"); + + // call backend + try { + const token = await getToken(); + const payload = { + startTime: startedAt, + endTime: end, + }; + const response = await fetch(`${baseUrl}/prod/add?type=stop`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + const resData = await response.json(); + if (!resData.success) toast.error("Error while saving time"); + } catch (err) { + console.error("Pause error", err); + toast.error("Error while saving time"); + } + setStartedAt(null); +}; + +const stop = async () => { + const end = Date.now(); + + if (startedAt) { + const sec = Math.floor((end - startedAt) / 1000); if (timerType === TimerTypes.WORK) { - setWorkAccum(v => v + sec); setSessionWorkAccum(v => v + sec); - } else { - setBreakAccum(v => v + sec); } - setStartedAt(null); - setStatus("paused"); - }; - - const stop = () => { - if(startedAt) { - const sec = Math.floor((Date.now() - startedAt)/1000); - if(timerType === TimerTypes.WORK) { - setSessionWorkAccum(v => v+sec); - setWorkAccum(0); - } else { - setBreakAccum(0); - } - } - setStartedAt(null); - setStatus("idle"); - setTimerType(TimerTypes.WORK); - setWorkAccum(0); - setBreakAccum(0); - }; + } + + setStatus("idle"); + setTimerType(TimerTypes.WORK); + setWorkAccum(0); + setBreakAccum(0); + + try { + const token = await getToken(); + const payload = { + startTime: startedAt, + endTime: end, + }; + const response = await fetch(`${baseUrl}/prod/add?type=stop`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + const resData = await response.json(); + if (resData.success) toast.success("Work session saved!"); + } catch (err) { + console.error("Stop error", err); + toast.error("Error while saving time"); + } + setStartedAt(null); +}; const toggle = () => (status === "running" ? pause() : start()); From c5855a096da7262ee8b738cf1021ee140fa92403 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Mon, 25 Aug 2025 00:27:20 +0530 Subject: [PATCH 08/13] Created mini timer which shows at the top right corner of components, and allows stopping/pausing and playing the timer --- backend/src/routes/productivityRoutes.ts | 2 +- .../dashboard/global_components/miniTimer.tsx | 68 +++++++++++++++++++ .../main_screens/analytics_page/analytics.tsx | 14 ++-- .../dashboard/main_screens/home_page/home.tsx | 14 ++-- .../productivity_zone_page/productivity.tsx | 8 +-- 5 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/dashboard/global_components/miniTimer.tsx diff --git a/backend/src/routes/productivityRoutes.ts b/backend/src/routes/productivityRoutes.ts index b50046c..6d88561 100644 --- a/backend/src/routes/productivityRoutes.ts +++ b/backend/src/routes/productivityRoutes.ts @@ -102,7 +102,7 @@ pRouter.get("/get_time", requireAuth, async (c) => { } return acc; }, []); - return c.json({ dataFixed }, 200); + return c.json({ success: true, dataFixed }, 200); } catch (err) { console.error("An error occured while fetching durations per day =", err); return c.json({ message: "Internal Server Error" }, 500); diff --git a/frontend/src/components/dashboard/global_components/miniTimer.tsx b/frontend/src/components/dashboard/global_components/miniTimer.tsx new file mode 100644 index 0000000..ed2438e --- /dev/null +++ b/frontend/src/components/dashboard/global_components/miniTimer.tsx @@ -0,0 +1,68 @@ +import { useTimer } from "@/context/useTimer"; +import { useMemo } from "react"; +import { TimerTypes } from "@/helpers/helpers"; +import { Square, Pause, Play } from "lucide-react"; + +export default function MiniTimer() { + const { status, workTime, breakTime, timerType, start, pause, stop } = + useTimer(); + + const active = status !== "idle"; + const paused = status === "paused"; + + const handleStopPause = (type: "stop" | "pause") => { + if (type === "stop") stop(); + else pause(); + }; + + const display = useMemo(() => { + const t = Math.max( + 0, + Math.floor(timerType === TimerTypes.WORK ? workTime : breakTime) + ); + const minutes = Math.floor((t % 3600) / 60); + const seconds = t % 60; + + const mm = String(minutes).padStart(2, "0"); + const ss = String(seconds).padStart(2, "0"); + + return ( +

+ {mm}:{ss} +

+ ); + }, [workTime, breakTime, timerType]); + + // if timer is stopped do not show anything + if (!active) { + return; + } + + return ( +
+
{display}
+
+ handleStopPause("stop")} + > + + + {paused ? ( + + + + ) : ( + handleStopPause("pause")} + > + + + )} +
+
+ ); +} diff --git a/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx b/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx index 359f286..34d452d 100644 --- a/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx +++ b/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { toast } from "sonner"; import { Gauge, gaugeClasses } from "@mui/x-charts/Gauge"; import { RotateCcw } from "lucide-react"; +import MiniTimer from "../../global_components/miniTimer"; interface TaskData { total: number; @@ -49,11 +50,14 @@ export default function Analytics() { if (!data) { return (
-
-

Analytics

-

- View stats, check your progress of work completed. -

+
+
+

Analytics

+

+ View stats, check your progress of work completed. +

+
+

Loading...

diff --git a/frontend/src/components/dashboard/main_screens/home_page/home.tsx b/frontend/src/components/dashboard/main_screens/home_page/home.tsx index 44996ae..86427a5 100644 --- a/frontend/src/components/dashboard/main_screens/home_page/home.tsx +++ b/frontend/src/components/dashboard/main_screens/home_page/home.tsx @@ -1,16 +1,20 @@ import AITextArea from "./components/AITextArea"; import TaskList from "./components/TaskList"; import { useState } from "react"; +import MiniTimer from "../../global_components/miniTimer"; export default function Home() { const [isUpdated, setIsUpdated] = useState(false); return (
-
-

Dashboard

-

- Generate and manage your AI-powered tasks -

+
+
+

Dashboard

+

+ Generate and manage your AI-powered tasks +

+
+
diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx index 283c8b1..2c2c704 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -1,14 +1,8 @@ -import { useState, useRef, useEffect, useMemo } from "react"; +import { useMemo } from "react"; import Timer from "./components/timer"; import TimerButtons from "./components/buttons"; import { useTimer } from "@/context/useTimer"; - -const getNumber = (s: string | null, fallback: number) => { - const n = Number(s); - return Number.isFinite(n) && n >= 0 ? n : fallback; -}; - export default function Productivity() { const { From e26d9dd9098889d799b6a20024b6b085eee6eefa Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Mon, 25 Aug 2025 00:56:59 +0530 Subject: [PATCH 09/13] Added mini timer to analytics screen --- .../main_screens/analytics_page/analytics.tsx | 164 +++++++++--------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx b/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx index 34d452d..74f8216 100644 --- a/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx +++ b/frontend/src/components/dashboard/main_screens/analytics_page/analytics.tsx @@ -50,14 +50,11 @@ export default function Analytics() { if (!data) { return (
-
-
-

Analytics

+
+

Analytics

View stats, check your progress of work completed.

-
-

Loading...

@@ -66,85 +63,88 @@ export default function Analytics() { return (
-
-

Analytics

-

- View stats, check your progress of work completed. -

+
+
+

Analytics

+

+ View stats, check your progress of work completed. +

+
+
-
- setReload(v => !v)}> -
-
-

Completed Tasks

- `${value}/${valueMax}`} - sx={(theme) => ({ - [`& .${gaugeClasses.valueText}`]: { - fontSize: 30, - }, - [`& .${gaugeClasses.valueArc}`]: { - fill: "#00c950", - }, - [`& .${gaugeClasses.referenceArc}`]: { - fill: '#f0fdf4', - }, - })} - /> -
-
-

In Progress

- `${value}/${valueMax}`} - sx={(theme) => ({ - [`& .${gaugeClasses.valueText}`]: { - fontSize: 30, - }, - [`& .${gaugeClasses.valueArc}`]: { - fill: "#f0b100", - }, - [`& .${gaugeClasses.referenceArc}`]: { - fill: '#fef9c2', - }, - })} - /> -
-
-

Incomplete

- `${value}/${valueMax}`} - sx={(theme) => ({ - [`& .${gaugeClasses.valueText}`]: { - fontSize: 30, - }, - [`& .${gaugeClasses.valueArc}`]: { - fill: "#fb2c36", - }, - [`& .${gaugeClasses.referenceArc}`]: { - fill: '#ffe2e2', - }, - })} - /> -
+
+
setReload(v => !v)}> +
+
+

Completed Tasks

+ `${value}/${valueMax}`} + sx={(theme) => ({ + [`& .${gaugeClasses.valueText}`]: { + fontSize: 25, + }, + [`& .${gaugeClasses.valueArc}`]: { + fill: "#00c950", + }, + [`& .${gaugeClasses.referenceArc}`]: { + fill: "#f0fdf4", + }, + })} + /> +
+
+

In Progress

+ `${value}/${valueMax}`} + sx={(theme) => ({ + [`& .${gaugeClasses.valueText}`]: { + fontSize: 25, + }, + [`& .${gaugeClasses.valueArc}`]: { + fill: "#f0b100", + }, + [`& .${gaugeClasses.referenceArc}`]: { + fill: "#fef9c2", + }, + })} + /> +
+
+

Incomplete

+ `${value}/${valueMax}`} + sx={(theme) => ({ + [`& .${gaugeClasses.valueText}`]: { + fontSize: 25, + }, + [`& .${gaugeClasses.valueArc}`]: { + fill: "#fb2c36", + }, + [`& .${gaugeClasses.referenceArc}`]: { + fill: "#ffe2e2", + }, + })} + /> +
); From 99630f6f474f4452682c7dee346864c77cecfc31 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Mon, 25 Aug 2025 01:33:50 +0530 Subject: [PATCH 10/13] Added tool tip for pomodoro text --- .../productivity_zone_page/productivity.tsx | 77 +++++++++++++++---- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx index 2c2c704..a3c0f41 100644 --- a/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx +++ b/frontend/src/components/dashboard/main_screens/productivity_zone_page/productivity.tsx @@ -1,16 +1,24 @@ -import { useMemo } from "react"; +import { useState, useMemo } from "react"; import Timer from "./components/timer"; import TimerButtons from "./components/buttons"; import { useTimer } from "@/context/useTimer"; export default function Productivity() { - const { - status, timerType, workTime, breakTime, - start, pause, stop, - sessionIterations, sessionTotalWorkTime, formatHMS + status, + timerType, + workTime, + breakTime, + start, + pause, + stop, + sessionIterations, + sessionTotalWorkTime, + formatHMS, } = useTimer(); + const [pomodoroHover, setPomodoroHover] = useState(false); + const active = status !== "idle"; const paused = status === "paused"; @@ -20,20 +28,51 @@ export default function Productivity() { else stop(); }; - const timeDisplay = useMemo(() => formatHMS(sessionTotalWorkTime), [sessionTotalWorkTime]); + const timeDisplay = useMemo( + () => formatHMS(sessionTotalWorkTime), + [sessionTotalWorkTime] + ); return (

Productivity Zone

- Start studying and working with POMODORO technique and boost your productivity + Start studying and working with{" "} + setPomodoroHover(true)} + onPointerLeave={() => setPomodoroHover(false)} + > + POMODORO + {pomodoroHover && ( +

+

+ The Pomodoro Technique is a time-management method that uses a + timer to break work into 25-minute intervals, called + "pomodoros," separated by short 5-minute breaks.{" "} + + Read More + +

+
+ )} + {" "} + technique and boost your productivity

-
-
- +
+
+
+
-
-
- Iterations this session: {Math.floor(sessionIterations)} -
-
- Time spent in session: {timeDisplay} -
+
+
+ Iterations this session:{" "} + + {Math.floor(sessionIterations)} + +
+
+ Time spent in session:{" "} + {timeDisplay}
From 9676c4641d18665f669580dc420b1ccc28d9faf8 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Mon, 25 Aug 2025 02:08:22 +0530 Subject: [PATCH 11/13] Added long break type. to give 15 minutes break after 4th work set updated design for mini timer --- .../dashboard/global_components/miniTimer.tsx | 5 ++++- frontend/src/context/useTimer.tsx | 22 +++++++++++++++---- frontend/src/helpers/helpers.ts | 15 +++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/dashboard/global_components/miniTimer.tsx b/frontend/src/components/dashboard/global_components/miniTimer.tsx index ed2438e..695282d 100644 --- a/frontend/src/components/dashboard/global_components/miniTimer.tsx +++ b/frontend/src/components/dashboard/global_components/miniTimer.tsx @@ -40,7 +40,10 @@ export default function MiniTimer() { return (
-
{display}
+
+
{timerType === TimerTypes.WORK ? "📔Work Time" : timerType === TimerTypes.BREAK ? "😪Break Time" : "😴Long Break Time"}
+
{display}
+
(null); -const WORK_LIMIT = 30 * 60; // 30:00 +const WORK_LIMIT = 25 * 60; // 25:00 const BREAK_LIMIT = 5 * 60; // 05:00 +const LONG_BREAK_LIMIT = 15 * 60; // larger break time after 4 iterations const getNumber = (s: string | null, fallback: number) => { const n = Number(s); @@ -67,6 +69,8 @@ export function TimerProvider({ children }: { children: React.ReactNode }) { // Timestamp for current live segment (null if paused/idle) const [startedAt, setStartedAt] = useState(null); + const completedWorkBlocks = useRef(0); + // Persist (optional—remove if you don’t want persistence) useEffect(() => sessionStorage.setItem("work_time", String(workAccum)), [workAccum]); useEffect(() => sessionStorage.setItem("break_time", String(breakAccum)), [breakAccum]); @@ -120,11 +124,13 @@ export function TimerProvider({ children }: { children: React.ReactNode }) { // WORK -> BREAK if (timerType === TimerTypes.WORK && workTime >= WORK_LIMIT) { if (liveSec > 0) setSessionWorkAccum(v => v + liveSec); // commit live to session - setWorkAccum(0); // reset block - setTimerType(TimerTypes.BREAK); + setWorkAccum(0); // reset block + completedWorkBlocks.current += 1; + const isLong = completedWorkBlocks.current%4 === 0; + setTimerType(isLong ? TimerTypes.LONG_BREAK: TimerTypes.BREAK); setStartedAt(Date.now()); // start break immediately setSessionIterations(i => i + 0.5); - createNotification(breakNotifications[Math.floor(Math.random() * breakNotifications.length)]); + createNotification(isLong ? longBreakNotifications[Math.floor(Math.random() * longBreakNotifications.length)]: breakNotifications[Math.floor(Math.random() * breakNotifications.length)]) } // BREAK -> WORK @@ -135,6 +141,14 @@ export function TimerProvider({ children }: { children: React.ReactNode }) { setSessionIterations(i => i + 0.5); createNotification(workNotifications[Math.floor(Math.random() * workNotifications.length)]); } + + if(timerType === TimerTypes.LONG_BREAK && breakTime >= LONG_BREAK_LIMIT) { + setBreakAccum(0); + setTimerType(TimerTypes.WORK); + setStartedAt(Date.now()); + setSessionIterations(i => i+0.5); + createNotification(workNotifications[Math.floor(Math.random() * workNotifications.length)]); + } }, [status, timerType, workTime, breakTime, liveSec]); // Controls diff --git a/frontend/src/helpers/helpers.ts b/frontend/src/helpers/helpers.ts index 2ce4213..6866ff0 100644 --- a/frontend/src/helpers/helpers.ts +++ b/frontend/src/helpers/helpers.ts @@ -33,7 +33,22 @@ export const workNotifications = [ "👏 You’re doing great! Time to start the next work session." ]; +export const longBreakNotifications: string[] = [ + "🎉 You worked hard! Time to reset and refresh — take a long break 🛋️", + "👏 Awesome focus! Reward yourself with a longer rest now ☕", + "💪 Great job finishing four sessions. Breathe, relax, and recharge 🌿", + "🔥 Consistency pays off! Take a well-deserved long break 😌", + "🌟 You’ve earned it — step away and enjoy your long break 🌴", + "🏆 Fantastic work streak! Time to relax and clear your mind 🧘", + "💯 Well done! Recharge with a long break before the next round ⚡", + "🚀 Focus mode complete — now it’s relaxation mode. Take your long break 💤", + "🌈 Solid effort! Give your brain and body a proper reset 🧃", + "✨ Productivity achieved! Unwind and refresh during this long break 🎶" +]; + + export enum TimerTypes { WORK = "work", BREAK = "break", + LONG_BREAK = "long_break" } \ No newline at end of file From 56f3b416577829479bb5e3e86a0086a3a69a02e7 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Tue, 26 Aug 2025 18:57:36 +0530 Subject: [PATCH 12/13] Minor changes --- .../src/components/dashboard/global_components/miniTimer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/dashboard/global_components/miniTimer.tsx b/frontend/src/components/dashboard/global_components/miniTimer.tsx index 695282d..86e0e9f 100644 --- a/frontend/src/components/dashboard/global_components/miniTimer.tsx +++ b/frontend/src/components/dashboard/global_components/miniTimer.tsx @@ -36,7 +36,7 @@ export default function MiniTimer() { // if timer is stopped do not show anything if (!active) { return; - } + } return (
From 71c4cfe5e9cd3bc0430afddc69567cb3aa780891 Mon Sep 17 00:00:00 2001 From: Gourab Das Date: Fri, 29 Aug 2025 23:57:08 +0530 Subject: [PATCH 13/13] Added timezone while saving date --- backend/src/routes/productivityRoutes.ts | 26 ++++++++++++++++++++++-- frontend/src/context/useTimer.tsx | 4 +++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/productivityRoutes.ts b/backend/src/routes/productivityRoutes.ts index 6d88561..5635f5f 100644 --- a/backend/src/routes/productivityRoutes.ts +++ b/backend/src/routes/productivityRoutes.ts @@ -10,12 +10,23 @@ type AddBodyType = { // checked startTime: number; endTime: number; + timeZone?: string; }; +function isoDateInTimeZone(d: Date, timeZone: string | undefined) { + const dtf = new Intl.DateTimeFormat('en-CA', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + return dtf.format(d); +} + pRouter.post("/add", requireAuth, async (c) => { try { const { userId } = c.get("authData"); - const { startTime, endTime }: AddBodyType = await c.req.json(); + const { startTime, endTime, timeZone }: AddBodyType = await c.req.json(); if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) { return c.json({ message: "Start and end must be finite!" }, 400); @@ -26,7 +37,18 @@ pRouter.post("/add", requireAuth, async (c) => { const startDate = new Date(startTime); const endDate = new Date(endTime); - const dateOnly = startDate.toISOString().split("T")[0]; + const startDateOnly = isoDateInTimeZone(startDate, timeZone); + const endDateOnly = isoDateInTimeZone(endDate, timeZone); + + let dateOnly: string | undefined; + + if(Number.parseInt(startDateOnly.substring(8)) == Number.parseInt(endDateOnly.substring(8))) { + dateOnly = startDateOnly; + } else if (Number.parseInt(startDateOnly.substring(8)) < Number.parseInt(endDateOnly.substring(8))) { + dateOnly = endDateOnly; + } else { + return c.json({ success: false, message: "End date cannot be smaller than start date" }, 400); + } // checking for existing data const dates = await db diff --git a/frontend/src/context/useTimer.tsx b/frontend/src/context/useTimer.tsx index 5a757e3..94ce1d1 100644 --- a/frontend/src/context/useTimer.tsx +++ b/frontend/src/context/useTimer.tsx @@ -181,11 +181,13 @@ const pause = async () => { // call backend try { const token = await getToken(); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const payload = { startTime: startedAt, endTime: end, + timeZone, }; - const response = await fetch(`${baseUrl}/prod/add?type=stop`, { + const response = await fetch(`${baseUrl}/prod/add`, { method: "POST", headers: { "Authorization": `Bearer ${token}`,