diff --git a/package-lock.json b/package-lock.json index 4e73a04..ae075da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.2", + "chalk": "^5.6.2", "electron-store": "^11.0.2", "lucide-react": "^0.552.0", "os-utils": "^0.0.14", @@ -288,6 +289,21 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", @@ -3651,17 +3667,15 @@ } }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", "engines": { - "node": ">=4" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/check-error": { @@ -5236,6 +5250,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -5808,6 +5823,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7609,6 +7625,21 @@ "concat-map": "0.0.1" } }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/npm-run-all/node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -9293,6 +9324,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, diff --git a/package.json b/package.json index 354d205..281221b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.2", + "chalk": "^5.6.2", "electron-store": "^11.0.2", "lucide-react": "^0.552.0", "os-utils": "^0.0.14", diff --git a/src/electron/api/apiHandler.ts b/src/electron/api/apiHandler.ts index 41aaeef..50e321a 100644 --- a/src/electron/api/apiHandler.ts +++ b/src/electron/api/apiHandler.ts @@ -1,30 +1,30 @@ -// electron/ipc/apiHandler.ts -import { ipcMain } from 'electron'; -import api from '../api/axiosClient.js'; +// // electron/ipc/apiHandler.ts +// import { ipcMain } from 'electron'; +// import api from '../api/axiosClient.js'; -interface ApiRequestArgs { - method: 'get' | 'post' | 'put' | 'patch' | 'delete'; - url: string; - data?: any; - config?: Record; -} +// interface ApiRequestArgs { +// method: 'get' | 'post' | 'put' | 'patch' | 'delete'; +// url: string; +// data?: any; +// config?: Record; +// } -ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => { - try { - const response = await api.request({ - method: args.method, - url: args.url, - data: args.data, - ...args.config, - }); +// ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => { +// try { +// const response = await api.request({ +// method: args.method, +// url: args.url, +// data: args.data, +// ...args.config, +// }); - return { success: true, data: response }; - } catch (error: any) { - console.error('[API Handler Error]', error.message); - return { - success: false, - error: error.message, - status: error.response?.status, - }; - } -}); +// return { success: true, data: response }; +// } catch (error: any) { +// console.error('[API Handler Error]', error.message); +// return { +// success: false, +// error: error.message, +// status: error.response?.status, +// }; +// } +// }); diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index 9cc2209..9c7c96a 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -1,14 +1,34 @@ // electron/api/axiosClient.ts import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { getToken } from '../utils/tokenHelper.js'; +import chalk from "chalk"; const TOKEN_KEY = 'auth_token'; +const Reset = "\x1b[0m"; +const Bright = "\x1b[1m"; +const Dim = "\x1b[2m"; +const Underscore = "\x1b[4m"; +const Blink = "\x1b[5m"; +const Reverse = "\x1b[7m"; +const Hidden = "\x1b[8m"; + +const FgRed = "\x1b[31m"; +const FgGreen = "\x1b[32m"; +const FgYellow = "\x1b[33m"; +const FgBlue = "\x1b[34m"; +const FgMagenta = "\x1b[35m"; +const FgCyan = "\x1b[36m"; +const FgWhite = "\x1b[37m"; const api: AxiosInstance = axios.create({ baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1', // 🔁 your backend timeout: 10000, }); +console.log("Creating Axios Instance", axios.VERSION); +console.log("Axios .get reference:", api.get); +console.log("Axios .request reference:", api.request); + // --- Request Interceptor --- api.interceptors.request.use( async (config: AxiosRequestConfig): Promise => { @@ -18,7 +38,11 @@ api.interceptors.request.use( config.headers['ngrok-skip-browser-warning'] = 98098; } - console.log(`[Axios] ${config.method?.toUpperCase()} ${config.url}`); + // Log method, URL, and headers + console.log( + chalk.red(`[Axios] ${config.method?.toUpperCase()} ${config.url}`) + ); + console.log(chalk.blue("Headers: "), config.headers); // <-- log headers return config; }, (error) => Promise.reject(error) diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index bbccd1d..cf671e5 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -28,9 +28,14 @@ ipcMain.handle('login-request', async (event, credentials: { email: string, pass return handleLogin(credentials); }); -ipcMain.handle('logout-request', async () => { - (async () => { - console.log("Handle Logout listened") - await api.get("/logout"); - })() +ipcMain.handle("logout-request", async () => { + console.log("Logout handler triggered"); + try { + const res = await api.get("/logout"); + console.log("logout response",res) + return { success: true, data: res.data }; + } catch (err: any) { + console.error("Logout failed:", err); + return { success: false, error: err.message }; + } }); \ No newline at end of file diff --git a/src/electron/main.ts b/src/electron/main.ts index c30d46d..54b2d6c 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -1,21 +1,30 @@ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, session } from 'electron'; import { ipcMainHandle, ipcMainOn, isDev } from './util.js'; import { getStaticData, pollResources } from './resourceManager.js'; import { getPreloadPath, getUIPath } from './pathResolver.js'; -import "./ipc/ipcHandler.js" +import "./ipc/ipcHandler.js"; app.on('ready', async () => { - console.log("ready") + console.log("ready"); + const mainWindow = new BrowserWindow({ + width: 550, // fixed width + height: 750, // fixed height + resizable: false, // prevents resizing + fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar) + maximizable: false, // prevents maximize + autoHideMenuBar: true, + frame: process.platform === 'darwin' ? false : true, webPreferences: { preload: getPreloadPath(), }, - // disables default system frame (dont do this if you want a proper working menu bar) - frame: process.platform === 'darwin' ? false : true, - autoHideMenuBar: true }); + session.defaultSession.webRequest.onBeforeRequest((details, callback) => { + console.log('🌐 Request:', details.method, details.url); + callback({ cancel: false }); + }); if (isDev()) { mainWindow.loadURL('http://localhost:5123'); @@ -26,9 +35,8 @@ app.on('ready', async () => { pollResources(mainWindow); - ipcMainHandle('getStaticData', () => { - return getStaticData(); - }); + // IPC handlers + ipcMainHandle('getStaticData', () => getStaticData()); ipcMainOn('sendFrameAction', (payload) => { switch (payload) { @@ -36,7 +44,7 @@ app.on('ready', async () => { mainWindow.close(); break; case 'MAXIMIZE': - mainWindow.maximize(); + // prevent maximize break; case 'MINIMIZE': mainWindow.minimize(); @@ -44,21 +52,20 @@ app.on('ready', async () => { } }); - // createTray(mainWindow); handleCloseEvents(mainWindow); - // createMenu(mainWindow); - // await takeScreenshot(); }); + + function handleCloseEvents(mainWindow: BrowserWindow) { let willClose = false; mainWindow.on('close', (e) => { - if (willClose) { - return; - } + if (willClose) return; + e.preventDefault(); mainWindow.hide(); + if (app.dock) { app.dock.hide(); } @@ -72,3 +79,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) { willClose = false; }); } + + diff --git a/src/electron/preload.ts b/src/electron/preload.cts similarity index 83% rename from src/electron/preload.ts rename to src/electron/preload.cts index 0c417c0..04c6a9d 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.cts @@ -23,6 +23,7 @@ contextBridge.exposeInMainWorld('electron', { getToken: (TOKEN_KEY: string) => ipcRenderer.invoke('get-token', TOKEN_KEY), uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'), + } satisfies Window['electron']); function ipcInvoke( @@ -45,11 +46,4 @@ function ipcSend( payload: EventPayloadMapping[Key] ) { electron.ipcRenderer.send(key, payload); -} - - -contextBridge.exposeInMainWorld('electronAPI', { - getToken: (key: string) => ipcRenderer.invoke('get-token', key), - saveToken: (key: string, token: string) => ipcRenderer.invoke('save-token', key, token), - clearToken: (key: string) => ipcRenderer.invoke('clear-token', key), -}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 0b98ec9..d82fc7c 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -18,6 +18,10 @@ import { SettingsIcon, TimerIcon, HelpCircleIcon, + Pause, + ShieldBan, + ChevronDown, + Play, } from "lucide-react"; interface User { @@ -37,6 +41,9 @@ interface ProjectTrackingData { endTime: Date | null; isTracking: boolean; isPaused: boolean; + pauseStartTime: Date | null; + totalPausedTime: number; + accumulatedTime: number; // ✅ NEW: Stores time from previous sessions } interface TrackingState { @@ -54,12 +61,9 @@ interface TrackingState { projectsData: Project[]; } - - interface DashboardLayoutProps { user: User | null; setUser: React.Dispatch>; - // trackingState: TrackingState; } const DashboardLayout: React.FC = ({ setUser }) => { @@ -68,7 +72,6 @@ const DashboardLayout: React.FC = ({ setUser }) => { const navigate = useNavigate(); const location = useLocation(); - // Tracking states const [selectedProject, setSelectedProject] = useState(null); const [isTracking, setIsTracking] = useState(false); @@ -88,33 +91,17 @@ const DashboardLayout: React.FC = ({ setUser }) => { Map >(new Map()); - useEffect(() => { - const handleResize = () => { - const mobile = window.innerWidth < 768; - setIsMobile(mobile); - setSidebarOpen(!mobile); - }; - - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - const handleLogout = async () => { localStorage.removeItem("user"); setUser(null); - - console.log("calling Logout UI") - + console.log("calling Logout UI"); try { - const res = await window.electron.logout() - console.log(res) + const res = await window.electron.logout(); + console.log(res, "Logout done"); } catch (error) { - console.log(error) + console.error("Logout error:", error); } - - navigate("/login"); }; const toggleSidebar = () => setSidebarOpen(!isSidebarOpen); @@ -132,8 +119,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { { label: "Help & Support", path: "/dashboard/help-support", icon: }, ]; - - // Sync global states with selected project tracking data + // ✅ Sync global states with selected project tracking data useEffect(() => { if (selectedProject) { const projectData = projectTrackingMap.get(selectedProject.id); @@ -147,14 +133,57 @@ const DashboardLayout: React.FC = ({ setUser }) => { } }, [selectedProject, projectTrackingMap]); - // Start tracking - const handleStartTracking = (project: Project) => { - // Clear any existing timer - if (timerRef.current) { - clearInterval(timerRef.current); + // ✅ Timer effect - calculates elapsed time based on timestamps + accumulated time + useEffect(() => { + if (isTracking && !isPaused && selectedProject) { + timerRef.current = setInterval(() => { + setProjectTrackingMap((prev) => { + const updated = new Map(prev); + const current = updated.get(selectedProject.id); + + if (current && current.isTracking && !current.isPaused && current.startTime) { + const now = Date.now(); + const startTimestamp = current.startTime.getTime(); + const currentSessionTime = Math.floor((now - startTimestamp) / 1000); + const totalPausedTime = current.totalPausedTime || 0; + + // ✅ FIX: Add accumulated time from previous sessions + const calculatedElapsed = + (current.accumulatedTime || 0) + currentSessionTime - totalPausedTime; + + current.elapsedTime = calculatedElapsed; + updated.set(selectedProject.id, { ...current }); + + // Update global elapsedTime in sync + setElapsedTime(calculatedElapsed); + } + return updated; + }); + }, 1000); + } else { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } } - // Update project tracking map + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }; + }, [isTracking, isPaused, selectedProject]); + + // ✅ Start tracking + const handleStartTracking = (project: Project) => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + + const now = new Date(); + setProjectTrackingMap((prev) => { const updated = new Map(prev); const current = updated.get(project.id) || { @@ -163,46 +192,37 @@ const DashboardLayout: React.FC = ({ setUser }) => { endTime: null, isTracking: false, isPaused: false, + pauseStartTime: null, + totalPausedTime: 0, + accumulatedTime: 0, }; - const now = new Date(); + // ✅ Keep accumulated time from previous sessions current.startTime = now; current.isTracking = true; current.isPaused = false; + current.pauseStartTime = null; + current.totalPausedTime = 0; // Reset pause time for new session updated.set(project.id, current); + // Update global tracking states + setIsTracking(true); + setIsPaused(false); + setStartTime(now); + setElapsedTime(current.accumulatedTime); // Start from accumulated time + return updated; }); - - // Update global tracking states - setIsTracking(true); - setIsPaused(false); - setStartTime(new Date()); - - // Start the interval timer - timerRef.current = setInterval(() => { - setProjectTrackingMap((prev) => { - const updated = new Map(prev); - const current = updated.get(project.id); - - // Only increment when tracking AND not paused - if (current && current.isTracking && !current.isPaused) { - current.elapsedTime += 1; - updated.set(project.id, { ...current }); - } - return updated; - }); - }, 1000); }; - // Pause/Resume tracking + // ✅ Pause/Resume tracking with pause duration tracking const handlePauseResume = () => { if (!selectedProject) return; - // Get current state before toggling to avoid double-click issues const currentProjectData = projectTrackingMap.get(selectedProject.id); if (!currentProjectData || !currentProjectData.isTracking) return; + const now = new Date(); const newPausedState = !currentProjectData.isPaused; setProjectTrackingMap((prev) => { @@ -210,48 +230,63 @@ const DashboardLayout: React.FC = ({ setUser }) => { const current = updated.get(selectedProject.id); if (!current) return updated; - // Set the new pause state - current.isPaused = newPausedState; + if (newPausedState) { + // Starting pause + current.isPaused = true; + current.pauseStartTime = now; + } else { + // Resuming from pause + current.isPaused = false; + if (current.pauseStartTime) { + const pauseDuration = Math.floor( + (now.getTime() - current.pauseStartTime.getTime()) / 1000 + ); + current.totalPausedTime = (current.totalPausedTime || 0) + pauseDuration; + current.pauseStartTime = null; + } + } + updated.set(selectedProject.id, { ...current }); + setIsPaused(newPausedState); return updated; }); - - // Update global isPaused state immediately - setIsPaused(newPausedState); }; - // Stop tracking + // ✅ Stop tracking - PRESERVES elapsed time const handleStopTracking = () => { if (!selectedProject) return; - // Clear the timer if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } - // Update project tracking map + const now = new Date(); + setProjectTrackingMap((prev) => { const updated = new Map(prev); const current = updated.get(selectedProject.id); if (!current) return updated; - const now = new Date(); + // ✅ FIX: Store current elapsed time as accumulated time + current.accumulatedTime = current.elapsedTime; current.isTracking = false; current.isPaused = false; current.endTime = now; + current.startTime = null; // Clear startTime for stopped state updated.set(selectedProject.id, { ...current }); + // Update global states - keep elapsed time + setIsTracking(false); + setIsPaused(false); + setEndTime(now); + setElapsedTime(current.elapsedTime); // ✅ Keep the elapsed time + return updated; }); - // Reset global tracking states to allow selecting another project - setIsTracking(false); - setIsPaused(false); - setEndTime(new Date()); - - console.log("projectTrackingMap", projectTrackingMap) + console.log("Tracking stopped. Time preserved:", elapsedTime); }; // Cleanup timer on unmount @@ -263,16 +298,14 @@ const DashboardLayout: React.FC = ({ setUser }) => { }; }, []); - // Handle project selection + // ✅ Handle project selection const handleProjectSelect = (id: number) => { - // Only allow project selection when not tracking if (isTracking) return; const project = projectsData.find((p) => p.id === id); if (project) { setSelectedProject(project); - // Load existing tracking data if available const projectData = projectTrackingMap.get(id); if (projectData) { setElapsedTime(projectData.elapsedTime); @@ -281,7 +314,6 @@ const DashboardLayout: React.FC = ({ setUser }) => { setIsTracking(projectData.isTracking); setIsPaused(projectData.isPaused); } else { - // Reset states for new project setElapsedTime(0); setStartTime(null); setEndTime(null); @@ -306,132 +338,129 @@ const DashboardLayout: React.FC = ({ setUser }) => { handleProjectSelect, }; + const formatTime = (seconds: number) => { + const hrs = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + return `${hrs}h ${mins}m ${secs}s`; + }; + return ( -
- {/* Sidebar */} - + {/* Right Section */} +
+ {/* Timer */} +
+ {formatTime(elapsedTime || 0)} +
- {/* Mobile overlay */} - {isMobile && isSidebarOpen && ( -
- )} + {/* Alert Icon */} + + + - {/* Main Content */} -
- {/* Navbar */} - +
+ - {/* Page Content */} -
+ {/* Layout Body */} +
+ {/* Sidebar - Always visible */} + + + {/* Main Content */} +
-
+ ); }; -export default DashboardLayout; +export default DashboardLayout; \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 3263dd9..f6914de 100644 --- a/types.d.ts +++ b/types.d.ts @@ -34,7 +34,7 @@ interface Window { ) => UnsubscribeFunction; sendFrameAction: (payload: FrameWindowAction) => void; login: (credentials: { email: string; password: string }) => Promise; - logout: () => {}; + logout: () => Promise<{ success: boolean; error?: string }>; getToken: (tokenKey: string) => Promise; uploadScreenshot: () => {}; };