timer
This commit is contained in:
parent
ec383c109f
commit
ea74c00dde
|
|
@ -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",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"os-utils": "^0.0.14",
|
"os-utils": "^0.0.14",
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -3651,17 +3667,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": {
|
||||||
|
|
@ -5236,6 +5250,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"
|
||||||
}
|
}
|
||||||
|
|
@ -5808,6 +5823,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"
|
||||||
}
|
}
|
||||||
|
|
@ -7609,6 +7625,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",
|
||||||
|
|
@ -9293,6 +9324,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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"os-utils": "^0.0.14",
|
"os-utils": "^0.0.14",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,34 @@
|
||||||
// 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 = 'auth_token';
|
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({
|
const api: AxiosInstance = axios.create({
|
||||||
baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1', // 🔁 your backend
|
baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1', // 🔁 your backend
|
||||||
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> => {
|
||||||
|
|
@ -18,7 +38,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)
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,14 @@ ipcMain.handle('login-request', async (event, credentials: { email: string, pass
|
||||||
return handleLogin(credentials);
|
return handleLogin(credentials);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('logout-request', async () => {
|
ipcMain.handle("logout-request", async () => {
|
||||||
(async () => {
|
console.log("Logout handler triggered");
|
||||||
console.log("Handle Logout listened")
|
try {
|
||||||
await api.get("/logout");
|
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 };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1,21 +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(),
|
||||||
},
|
},
|
||||||
// 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');
|
||||||
|
|
@ -26,9 +35,8 @@ app.on('ready', async () => {
|
||||||
|
|
||||||
pollResources(mainWindow);
|
pollResources(mainWindow);
|
||||||
|
|
||||||
ipcMainHandle('getStaticData', () => {
|
// IPC handlers
|
||||||
return getStaticData();
|
ipcMainHandle('getStaticData', () => getStaticData());
|
||||||
});
|
|
||||||
|
|
||||||
ipcMainOn('sendFrameAction', (payload) => {
|
ipcMainOn('sendFrameAction', (payload) => {
|
||||||
switch (payload) {
|
switch (payload) {
|
||||||
|
|
@ -36,7 +44,7 @@ app.on('ready', async () => {
|
||||||
mainWindow.close();
|
mainWindow.close();
|
||||||
break;
|
break;
|
||||||
case 'MAXIMIZE':
|
case 'MAXIMIZE':
|
||||||
mainWindow.maximize();
|
// prevent maximize
|
||||||
break;
|
break;
|
||||||
case 'MINIMIZE':
|
case 'MINIMIZE':
|
||||||
mainWindow.minimize();
|
mainWindow.minimize();
|
||||||
|
|
@ -44,21 +52,20 @@ app.on('ready', async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
|
|
@ -72,3 +79,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
|
||||||
willClose = false;
|
willClose = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||||
|
|
||||||
getToken: (TOKEN_KEY: string) => ipcRenderer.invoke('get-token', TOKEN_KEY),
|
getToken: (TOKEN_KEY: string) => ipcRenderer.invoke('get-token', TOKEN_KEY),
|
||||||
uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'),
|
uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'),
|
||||||
|
|
||||||
} satisfies Window['electron']);
|
} satisfies Window['electron']);
|
||||||
|
|
||||||
function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
||||||
|
|
@ -46,10 +47,3 @@ function ipcSend<Key extends keyof EventPayloadMapping>(
|
||||||
) {
|
) {
|
||||||
electron.ipcRenderer.send(key, payload);
|
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),
|
|
||||||
});
|
|
||||||
|
|
@ -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,33 +91,17 @@ 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 () => {
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
console.log("calling Logout UI");
|
||||||
console.log("calling Logout UI")
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await window.electron.logout()
|
const res = await window.electron.logout();
|
||||||
console.log(res)
|
console.log(res, "Logout done");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.error("Logout error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate("/login");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
||||||
|
|
@ -132,8 +119,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);
|
||||||
|
|
@ -147,14 +133,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) {
|
||||||
if (timerRef.current) {
|
timerRef.current = setInterval(() => {
|
||||||
clearInterval(timerRef.current);
|
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) => {
|
setProjectTrackingMap((prev) => {
|
||||||
const updated = new Map(prev);
|
const updated = new Map(prev);
|
||||||
const current = updated.get(project.id) || {
|
const current = updated.get(project.id) || {
|
||||||
|
|
@ -163,46 +192,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);
|
||||||
|
|
||||||
|
// Update global tracking states
|
||||||
|
setIsTracking(true);
|
||||||
|
setIsPaused(false);
|
||||||
|
setStartTime(now);
|
||||||
|
setElapsedTime(current.accumulatedTime); // Start from accumulated time
|
||||||
|
|
||||||
return updated;
|
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 = () => {
|
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) => {
|
||||||
|
|
@ -210,48 +230,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
|
||||||
|
|
@ -263,16 +298,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);
|
||||||
|
|
@ -281,7 +314,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);
|
||||||
|
|
@ -306,131 +338,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
|
<button
|
||||||
? "translate-x-0 w-64"
|
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
||||||
: "-translate-x-full w-64"
|
onClick={handleStopTracking}
|
||||||
: isSidebarOpen
|
title="Stop Tracking"
|
||||||
? "w-50"
|
>
|
||||||
: "w-20"
|
<Pause size={16} />
|
||||||
}
|
</button>
|
||||||
`}
|
) : (
|
||||||
>
|
<button
|
||||||
{/* Sidebar Header */}
|
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
||||||
<div className="h-14 text-xl font-bold flex justify-between items-center border-b border-base-300">
|
onClick={() => handleStartTracking(selectedProject)}
|
||||||
{isSidebarOpen ? (
|
title="Start Tracking"
|
||||||
<span className="whitespace-nowrap mx-auto">VA Tracker</span>
|
>
|
||||||
|
<Play size={16} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<span className="text-primary font-extrabold mx-auto">VA</span>
|
|
||||||
)}
|
|
||||||
{isMobile && (
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleSidebar}
|
className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed"
|
||||||
className="btn btn-ghost btn-sm md:hidden"
|
disabled
|
||||||
aria-label="Close sidebar"
|
title="Select a project to start"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<Play size={16} />
|
||||||
</button>
|
</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">
|
||||||
{menuItems.map((item) => (
|
{/* Timer */}
|
||||||
<li key={item.label}>
|
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
|
||||||
<NavLink
|
{formatTime(elapsedTime || 0)}
|
||||||
to={item.path}
|
</div>
|
||||||
end={item.exact}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex items-center gap-3 my-1 font-medium transition-all duration-150 ${isActive
|
|
||||||
? "bg-primary text-primary-content rounded-lg"
|
|
||||||
: "hover:bg-base-300 rounded-lg"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={closeSidebarOnMobile}
|
|
||||||
>
|
|
||||||
<div className="min-w-[24px] flex justify-center">{item.icon}</div>
|
|
||||||
{isSidebarOpen && <span>{item.label}</span>}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Mobile overlay */}
|
{/* Alert Icon */}
|
||||||
{isMobile && isSidebarOpen && (
|
<span className="text-error">
|
||||||
<div
|
<ShieldBan size={18} />
|
||||||
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden"
|
</span>
|
||||||
onClick={closeSidebarOnMobile}
|
|
||||||
></div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Dropdown */}
|
||||||
<div className="flex flex-col flex-1 bg-base-200 transition-all duration-300 ease-in-out">
|
<div className="dropdown dropdown-end">
|
||||||
{/* Navbar */}
|
<div
|
||||||
<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">
|
tabIndex={0}
|
||||||
{/* Left */}
|
role="button"
|
||||||
<div className="flex items-center gap-2">
|
className="btn btn-sm btn-outline btn-primary p-1.5 rounded-lg"
|
||||||
<button
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
className="btn btn-ghost btn-square"
|
|
||||||
aria-label="Toggle sidebar"
|
|
||||||
>
|
>
|
||||||
<Menu size={20} />
|
<ChevronDown />
|
||||||
</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>
|
||||||
|
<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>
|
||||||
</nav>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{/* Page Content */}
|
{/* Layout Body */}
|
||||||
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
<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) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.label}
|
||||||
|
to={item.path}
|
||||||
|
end={item.exact}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${
|
||||||
|
isActive
|
||||||
|
? "bg-primary text-primary-content"
|
||||||
|
: "hover:bg-base-300 text-base-content"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
<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"
|
||||||
|
title="Logout"
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
|
||||||
<Outlet context={trackingState} />
|
<Outlet context={trackingState} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ interface Window {
|
||||||
) => UnsubscribeFunction;
|
) => UnsubscribeFunction;
|
||||||
sendFrameAction: (payload: FrameWindowAction) => void;
|
sendFrameAction: (payload: FrameWindowAction) => void;
|
||||||
login: (credentials: { email: string; password: string }) => Promise<any>;
|
login: (credentials: { email: string; password: string }) => Promise<any>;
|
||||||
logout: () => {};
|
logout: () => Promise<{ success: boolean; error?: string }>;
|
||||||
getToken: (tokenKey: string) => Promise<string | null>;
|
getToken: (tokenKey: string) => Promise<string | null>;
|
||||||
uploadScreenshot: () => {};
|
uploadScreenshot: () => {};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue