Merge branch 'merge'

This commit is contained in:
BhagwanSingh 2025-11-07 15:45:39 +05:30
commit 3f190f333b
7 changed files with 327 additions and 249 deletions

52
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2", "axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"get-windows": "^9.2.3", "get-windows": "^9.2.3",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
@ -288,6 +289,21 @@
"node": ">=6.9.0" "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": { "node_modules/@babel/parser": {
"version": "7.24.5", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
@ -3832,17 +3848,15 @@
} }
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "2.4.2", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true, "license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": { "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": { "node_modules/check-error": {
@ -5422,6 +5436,7 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
@ -6319,6 +6334,7 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@ -8168,6 +8184,21 @@
"concat-map": "0.0.1" "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": { "node_modules/npm-run-all/node_modules/cross-spawn": {
"version": "6.0.5", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -9863,6 +9894,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
}, },

View File

@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2", "axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"get-windows": "^9.2.3", "get-windows": "^9.2.3",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",

View File

@ -1,30 +1,30 @@
// electron/ipc/apiHandler.ts // // electron/ipc/apiHandler.ts
import { ipcMain } from 'electron'; // import { ipcMain } from 'electron';
import api from '../api/axiosClient.js'; // import api from '../api/axiosClient.js';
interface ApiRequestArgs { // interface ApiRequestArgs {
method: 'get' | 'post' | 'put' | 'patch' | 'delete'; // method: 'get' | 'post' | 'put' | 'patch' | 'delete';
url: string; // url: string;
data?: any; // data?: any;
config?: Record<string, any>; // config?: Record<string, any>;
} // }
ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => { // ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
try { // try {
const response = await api.request({ // const response = await api.request({
method: args.method, // method: args.method,
url: args.url, // url: args.url,
data: args.data, // data: args.data,
...args.config, // ...args.config,
}); // });
return { success: true, data: response }; // return { success: true, data: response };
} catch (error: any) { // } catch (error: any) {
console.error('[API Handler Error]', error.message); // console.error('[API Handler Error]', error.message);
return { // return {
success: false, // success: false,
error: error.message, // error: error.message,
status: error.response?.status, // status: error.response?.status,
}; // };
} // }
}); // });

View File

@ -1,6 +1,7 @@
// electron/api/axiosClient.ts // electron/api/axiosClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken } from '../utils/tokenHelper.js'; import { getToken } from '../utils/tokenHelper.js';
import chalk from "chalk";
const TOKEN_KEY = 'ACCESS_TOKEN'; const TOKEN_KEY = 'ACCESS_TOKEN';
@ -9,6 +10,10 @@ const api: AxiosInstance = axios.create({
// timeout: 10000, // 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 --- // --- Request Interceptor ---
api.interceptors.request.use( api.interceptors.request.use(
async (config: AxiosRequestConfig): Promise<any> => { async (config: AxiosRequestConfig): Promise<any> => {
@ -20,7 +25,11 @@ api.interceptors.request.use(
config.headers['ngrok-skip-browser-warning'] = 98098; 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; return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)

View File

@ -1,27 +1,30 @@
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow, session } from 'electron';
import { ipcMainHandle, ipcMainOn, isDev } from './util.js'; import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
import { getStaticData, pollResources } from './resourceManager.js'; import { getStaticData, pollResources } from './resourceManager.js';
import { getPreloadPath, getUIPath } from './pathResolver.js'; import { getPreloadPath, getUIPath } from './pathResolver.js';
import "./ipc/ipcHandler.js" import "./ipc/ipcHandler.js";
app.on('ready', async () => { app.on('ready', async () => {
console.log("ready") console.log("ready");
const mainWindow = new BrowserWindow({ 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: { webPreferences: {
preload: getPreloadPath(), preload: getPreloadPath(),
}, },
resizable: false, // disables resizing (no maximize)
minimizable: false, // disables minimize button
maximizable: false, // disables maximize button
closable: true,
alwaysOnTop: true,
// 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()) { if (isDev()) {
mainWindow.loadURL('http://localhost:5123'); mainWindow.loadURL('http://localhost:5123');
@ -32,39 +35,37 @@ app.on('ready', async () => {
pollResources(mainWindow); pollResources(mainWindow);
ipcMainHandle('getStaticData', () => { // IPC handlers
return getStaticData(); ipcMainHandle('getStaticData', () => getStaticData());
ipcMainOn('sendFrameAction', (payload) => {
switch (payload) {
case 'CLOSE':
mainWindow.close();
break;
case 'MAXIMIZE':
// prevent maximize
break;
case 'MINIMIZE':
mainWindow.minimize();
break;
}
}); });
// ipcMainOn('sendFrameAction', (payload) => {
// switch (payload) {
// case 'CLOSE':
// mainWindow.close();
// break;
// case 'MAXIMIZE':
// mainWindow.maximize();
// break;
// case 'MINIMIZE':
// mainWindow.minimize();
// break;
// }
// });
// createTray(mainWindow);
handleCloseEvents(mainWindow); handleCloseEvents(mainWindow);
// createMenu(mainWindow);
// await takeScreenshot();
}); });
function handleCloseEvents(mainWindow: BrowserWindow) { function handleCloseEvents(mainWindow: BrowserWindow) {
let willClose = false; let willClose = false;
mainWindow.on('close', (e) => { mainWindow.on('close', (e) => {
if (willClose) { if (willClose) return;
return;
}
e.preventDefault(); e.preventDefault();
mainWindow.hide(); mainWindow.hide();
if (app.dock) { if (app.dock) {
app.dock.hide(); app.dock.hide();
} }
@ -78,3 +79,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
willClose = false; willClose = false;
}); });
} }

View File

@ -18,6 +18,10 @@ import {
SettingsIcon, SettingsIcon,
TimerIcon, TimerIcon,
HelpCircleIcon, HelpCircleIcon,
Pause,
ShieldBan,
ChevronDown,
Play,
} from "lucide-react"; } from "lucide-react";
interface User { interface User {
@ -37,6 +41,9 @@ interface ProjectTrackingData {
endTime: Date | null; endTime: Date | null;
isTracking: boolean; isTracking: boolean;
isPaused: boolean; isPaused: boolean;
pauseStartTime: Date | null;
totalPausedTime: number;
accumulatedTime: number; // ✅ NEW: Stores time from previous sessions
} }
interface TrackingState { interface TrackingState {
@ -54,12 +61,9 @@ interface TrackingState {
projectsData: Project[]; projectsData: Project[];
} }
interface DashboardLayoutProps { interface DashboardLayoutProps {
user: User | null; user: User | null;
setUser: React.Dispatch<React.SetStateAction<User | null>>; setUser: React.Dispatch<React.SetStateAction<User | null>>;
// trackingState: TrackingState;
} }
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => { const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
@ -68,7 +72,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
// Tracking states // Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null); const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false); const [isTracking, setIsTracking] = useState(false);
@ -88,18 +91,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
Map<number, ProjectTrackingData> Map<number, ProjectTrackingData>
>(new 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 () => { const handleLogout = async () => {
try { try {
const res = await window.electron.logout() const res = await window.electron.logout()
@ -109,7 +100,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
navigate("/login"); navigate("/login");
} }
} catch (error) { } catch (error) {
console.log(error) console.error("Logout error:", error);
} }
}; };
@ -138,8 +129,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> }, { label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
]; ];
// ✅ Sync global states with selected project tracking data
// Sync global states with selected project tracking data
useEffect(() => { useEffect(() => {
if (selectedProject) { if (selectedProject) {
const projectData = projectTrackingMap.get(selectedProject.id); const projectData = projectTrackingMap.get(selectedProject.id);
@ -153,14 +143,57 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
} }
}, [selectedProject, projectTrackingMap]); }, [selectedProject, projectTrackingMap]);
// Start tracking // ✅ Timer effect - calculates elapsed time based on timestamps + accumulated time
const handleStartTracking = (project: Project) => { useEffect(() => {
// Clear any existing timer 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) { if (timerRef.current) {
clearInterval(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) => { setProjectTrackingMap((prev) => {
const updated = new Map(prev); const updated = new Map(prev);
const current = updated.get(project.id) || { const current = updated.get(project.id) || {
@ -169,46 +202,37 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
endTime: null, endTime: null,
isTracking: false, isTracking: false,
isPaused: false, isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: 0,
}; };
const now = new Date(); // ✅ Keep accumulated time from previous sessions
current.startTime = now; current.startTime = now;
current.isTracking = true; current.isTracking = true;
current.isPaused = false; current.isPaused = false;
current.pauseStartTime = null;
current.totalPausedTime = 0; // Reset pause time for new session
updated.set(project.id, current); updated.set(project.id, current);
return updated;
});
// Update global tracking states // Update global tracking states
setIsTracking(true); setIsTracking(true);
setIsPaused(false); setIsPaused(false);
setStartTime(new Date()); setStartTime(now);
setElapsedTime(current.accumulatedTime); // Start from accumulated time
// 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; return updated;
}); });
}, 1000);
}; };
// Pause/Resume tracking // ✅ Pause/Resume tracking with pause duration tracking
const handlePauseResume = () => { const handlePauseResume = () => {
if (!selectedProject) return; if (!selectedProject) return;
// Get current state before toggling to avoid double-click issues
const currentProjectData = projectTrackingMap.get(selectedProject.id); const currentProjectData = projectTrackingMap.get(selectedProject.id);
if (!currentProjectData || !currentProjectData.isTracking) return; if (!currentProjectData || !currentProjectData.isTracking) return;
const now = new Date();
const newPausedState = !currentProjectData.isPaused; const newPausedState = !currentProjectData.isPaused;
setProjectTrackingMap((prev) => { setProjectTrackingMap((prev) => {
@ -216,48 +240,63 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const current = updated.get(selectedProject.id); const current = updated.get(selectedProject.id);
if (!current) return updated; if (!current) return updated;
// Set the new pause state if (newPausedState) {
current.isPaused = 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 }); updated.set(selectedProject.id, { ...current });
setIsPaused(newPausedState);
return updated; return updated;
}); });
// Update global isPaused state immediately
setIsPaused(newPausedState);
}; };
// Stop tracking // ✅ Stop tracking - PRESERVES elapsed time
const handleStopTracking = () => { const handleStopTracking = () => {
if (!selectedProject) return; if (!selectedProject) return;
// Clear the timer
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
timerRef.current = null; timerRef.current = null;
} }
// Update project tracking map const now = new Date();
setProjectTrackingMap((prev) => { setProjectTrackingMap((prev) => {
const updated = new Map(prev); const updated = new Map(prev);
const current = updated.get(selectedProject.id); const current = updated.get(selectedProject.id);
if (!current) return updated; if (!current) return updated;
const now = new Date(); // ✅ FIX: Store current elapsed time as accumulated time
current.accumulatedTime = current.elapsedTime;
current.isTracking = false; current.isTracking = false;
current.isPaused = false; current.isPaused = false;
current.endTime = now; current.endTime = now;
current.startTime = null; // Clear startTime for stopped state
updated.set(selectedProject.id, { ...current }); 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; return updated;
}); });
// Reset global tracking states to allow selecting another project console.log("Tracking stopped. Time preserved:", elapsedTime);
setIsTracking(false);
setIsPaused(false);
setEndTime(new Date());
console.log("projectTrackingMap", projectTrackingMap)
}; };
// Cleanup timer on unmount // Cleanup timer on unmount
@ -269,16 +308,14 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}; };
}, []); }, []);
// Handle project selection // ✅ Handle project selection
const handleProjectSelect = (id: number) => { const handleProjectSelect = (id: number) => {
// Only allow project selection when not tracking
if (isTracking) return; if (isTracking) return;
const project = projectsData.find((p) => p.id === id); const project = projectsData.find((p) => p.id === id);
if (project) { if (project) {
setSelectedProject(project); setSelectedProject(project);
// Load existing tracking data if available
const projectData = projectTrackingMap.get(id); const projectData = projectTrackingMap.get(id);
if (projectData) { if (projectData) {
setElapsedTime(projectData.elapsedTime); setElapsedTime(projectData.elapsedTime);
@ -287,7 +324,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
setIsTracking(projectData.isTracking); setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused); setIsPaused(projectData.isPaused);
} else { } else {
// Reset states for new project
setElapsedTime(0); setElapsedTime(0);
setStartTime(null); setStartTime(null);
setEndTime(null); setEndTime(null);
@ -312,131 +348,128 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
handleProjectSelect, 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 ( return (
<div className="flex h-screen bg-base-200 overflow-hidden"> <>
{/* Sidebar */} {/* Header */}
<aside <header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
className={` {/* Left Section */}
fixed md:relative inset-y-0 left-0 z-40 transform <div className="flex items-center space-x-3">
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out {selectedProject ? (
${isMobile isTracking ? (
? isSidebarOpen
? "translate-x-0 w-64"
: "-translate-x-full w-64"
: isSidebarOpen
? "w-50"
: "w-20"
}
`}
>
{/* Sidebar Header */}
<div className="h-14 text-xl font-bold flex justify-between items-center border-b border-base-300">
{isSidebarOpen ? (
<span className="whitespace-nowrap mx-auto">VA Tracker</span>
) : (
<span className="text-primary font-extrabold mx-auto">VA</span>
)}
{isMobile && (
<button <button
onClick={toggleSidebar} className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
className="btn btn-ghost btn-sm md:hidden" onClick={handleStopTracking}
aria-label="Close sidebar" title="Stop Tracking"
> >
<X size={20} /> <Pause size={16} />
</button> </button>
) : (
<button
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
onClick={() => handleStartTracking(selectedProject)}
title="Start Tracking"
>
<Play size={16} />
</button>
)
) : (
<button
className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed"
disabled
title="Select a project to start"
>
<Play size={16} />
</button>
)}
{selectedProject ? (
<span className="ml-2 text-sm text-base-content/70">
{selectedProject.projectName}
</span>
) : (
<span className="ml-2 text-sm text-base-content/40 italic">
No Project Selected
</span>
)} )}
</div> </div>
{/* Menu */} {/* Right Section */}
<ul className="menu px-2 py-4"> <div className="flex items-center space-x-3">
{/* Timer */}
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
{formatTime(elapsedTime || 0)}
</div>
{/* Alert Icon */}
<span className="text-error">
<ShieldBan size={18} />
</span>
{/* Dropdown */}
<div className="dropdown dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-sm btn-outline btn-primary p-1.5 rounded-lg"
>
<ChevronDown />
</div>
<ul
tabIndex={0}
className="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-40 mt-2"
>
<li><a>View Logs</a></li>
<li><a>Settings</a></li>
</ul>
</div>
</div>
</header>
{/* Layout Body */}
<div className="flex h-[calc(100vh-3rem)] bg-base-200 overflow-hidden">
{/* Sidebar - Always visible */}
<aside
className="w-20 bg-base-100 border-r border-base-300 flex-shrink-0
flex flex-col items-center py-4 space-y-4"
>
{menuItems.map((item) => ( {menuItems.map((item) => (
<li key={item.label}>
<NavLink <NavLink
key={item.label}
to={item.path} to={item.path}
end={item.exact} end={item.exact}
className={({ isActive }) => className={({ isActive }) =>
`flex items-center gap-3 my-1 font-medium transition-all duration-150 ${isActive `flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${
? "bg-primary text-primary-content rounded-lg" isActive
: "hover:bg-base-300 rounded-lg" ? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
}` }`
} }
onClick={closeSidebarOnMobile}
> >
<div className="min-w-[24px] flex justify-center">{item.icon}</div> {item.icon}
{isSidebarOpen && <span>{item.label}</span>}
</NavLink> </NavLink>
</li>
))} ))}
</ul> <button
onClick={handleLogout}
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer"
title="Logout"
>
<LogOut size={18} />
</button>
</aside> </aside>
{/* Mobile overlay */}
{isMobile && isSidebarOpen && (
<div
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden"
onClick={closeSidebarOnMobile}
></div>
)}
{/* Main Content */} {/* Main Content */}
<div className="flex flex-col flex-1 bg-base-200 transition-all duration-300 ease-in-out"> <main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
{/* Navbar */}
<nav className="h-14 bg-base-100 border-b border-t border-base-300 px-4 flex justify-between items-center sticky top-0 z-30">
{/* Left */}
<div className="flex items-center gap-2">
<button
onClick={toggleSidebar}
className="btn btn-ghost btn-square"
aria-label="Toggle sidebar"
>
<Menu size={20} />
</button>
<span className="text-lg font-semibold md:hidden">
{location.pathname.includes("projects")
? "Projects"
: location.pathname.includes("leave-adjustments")
? "Leave & Adjustments"
: "Dashboard"}
</span>
</div>
{/* Right */}
<div className="flex items-center gap-1">
<button className="btn btn-ghost btn-circle">
<Bell size={20} />
</button>
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-8 h-8 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
<User size={18} className="text-base-content" />
</div>
</label>
<ul
tabIndex={0}
className="mt-3 p-2 shadow menu menu-sm dropdown-content bg-base-200 rounded-box w-52"
>
<li>
<a>
<Settings size={16} /> Settings
</a>
</li>
<li>
<a onClick={handleLogout} className="text-error">
<LogOut size={16} /> Logout
</a>
</li>
</ul>
</div>
</div>
</nav>
{/* Page Content */}
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
<Outlet context={trackingState} /> <Outlet context={trackingState} />
</main> </main>
</div> </div>
</div> </>
); );
}; };