axios setup

This commit is contained in:
JaiPandey425 2025-11-06 16:40:43 +05:30
parent e11d4e52f2
commit 6783fba264
12 changed files with 637 additions and 562 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<!-- <link rel="icon" type="image/svg+xml" href="" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Tracker</title>
<meta

View File

@ -0,0 +1,30 @@
// 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<string, any>;
}
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,
};
}
});

View File

@ -0,0 +1,38 @@
// electron/api/axiosClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken } from '../utils/tokenHelper.js';
const TOKEN_KEY = 'auth_token';
const api: AxiosInstance = axios.create({
baseURL: 'https://api.example.com', // 🔁 your backend
timeout: 10000,
});
// --- Request Interceptor ---
api.interceptors.request.use(
async (config: AxiosRequestConfig) : Promise<any> => {
const token = await getToken(TOKEN_KEY);
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
console.log(`[Axios] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error) => Promise.reject(error)
);
// --- Response Interceptor ---
api.interceptors.response.use(
(response: AxiosResponse) => response.data,
async (error) => {
console.error('[Axios Error]', error.message);
if (error.response?.status === 401) {
console.warn('Token expired — trigger refresh logic if needed');
}
return Promise.reject(error);
}
);
export default api;

View File

@ -0,0 +1,28 @@
// electron/ipc/tokenHandler.ts
import { ipcMain } from 'electron';
import { getToken, saveToken, clearToken } from '../utils/tokenHelper.js';
import { handleLogin, takeScreenshot } from '../utils/utilityFunctions.js';
ipcMain.handle('get-token', async (_, TOKEN_KEY: string) => {
return await getToken(TOKEN_KEY);
});
ipcMain.handle('save-token', async (_, TOKEN_KEY: string, token: string) => {
await saveToken(TOKEN_KEY, token);
return true;
});
ipcMain.handle('clear-token', async (_, TOKEN_KEY: string) => {
await clearToken(TOKEN_KEY);
return true;
});
ipcMain.handle('upload-screenshot', async () => {
takeScreenshot()
})
// Handle login request from renderer
ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => {
return handleLogin(credentials);
});

View File

@ -1,15 +1,9 @@
import { app, BrowserWindow, Menu, desktopCapturer, ipcMain, safeStorage } from 'electron';
import { app, BrowserWindow } from 'electron';
import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
import { getStaticData, pollResources } from './resourceManager.js';
import { getPreloadPath, getUIPath } from './pathResolver.js';
import { createTray } from './tray.js';
import { createMenu } from './menu.js';
import Store from 'electron-store';
import * as fs from "fs";
import path from "path";
import axios from 'axios';
import sharp from 'sharp';
import "./ipc/ipcHandler.js"
app.on('ready', async () => {
console.log("ready")
@ -56,132 +50,6 @@ app.on('ready', async () => {
// await takeScreenshot();
});
// Blur using an offscreen canvas
async function blurAndCompressBase64Image(base64: string) : Promise<Buffer> {
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
const outputBuffer = await sharp(buffer)
.blur(8) // adjust blur radius here
.jpeg({ quality: 70 }) // compress output
.toBuffer();
return outputBuffer;
}
async function takeScreenshot() {
// const sources = await desktopCapturer.getSources({ types: ["screen"] });
const sources = await desktopCapturer.getSources({
types: ['window'], thumbnailSize: {
height: 768,
width: 1366
}
});
// Capture the first screen (you can choose others by filtering sources)
const screen = sources[0];
let image = screen.thumbnail.toDataURL();
console.log("blurAndCompressBase64Image Calling")
const blurredBuffer = await blurAndCompressBase64Image(image);
const folderPath = "C:\\Users\\Public\\vatracker\\screenshots";
// Ensure directory exists
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`);
fs.writeFileSync(filePath, blurredBuffer);
console.log(`Screenshot saved at: ${filePath}`);
}
ipcMain.handle('upload-screenshot', async () => {
takeScreenshot()
})
async function saveToken(accessToken: string, refreshToken: string | null = null) {
const encryptedAccessToken = safeStorage.encryptString(accessToken);
let encryptedRefreshToken = null;
if (refreshToken != null) {
encryptedRefreshToken = safeStorage.encryptString(refreshToken);
}
// Store encrypted token (example: using a file or electron-store)
// For simplicity, using Electron's built-in localStorage is not available in main, so use electron-store or filesystem.
// Here assume electron-store is already installed and set up:
const store = new Store();
store.set("access_token", encryptedAccessToken.toString('base64'));
if (encryptedRefreshToken != null) {
store.set("refresh_token", encryptedRefreshToken.toString('base64'));
}
}
async function getToken(TOKEN_KEY: string) {
const store = new Store();
const encryptedBase64 = store.get(TOKEN_KEY) as string | undefined;
if (!encryptedBase64) return null;
const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
return safeStorage.decryptString(encryptedBuffer);
}
ipcMain.handle('get-token', async (event, TOKEN_KEY) => {
// return decrypted token from secure storage
return await getToken(TOKEN_KEY);
});
ipcMain.handle('save-token', async (event, token) => {
await saveToken(token);
return true;
});
// Handle login request from renderer
ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => {
try {
// Call your login API
console.log("🟡 login Request received", credentials);
try {
console.log("➡️ Calling API...");
const response = await axios.post(
'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login',
{ userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 } }
);
const { accessToken, refreshToken } = response.data;
await saveToken(accessToken, refreshToken);
return { success: true, user: response.data.user };
} catch (error) {
if (error instanceof Error) {
if ("response" in error && typeof (error.response) == "object" && error.response !== null && ("data" in error.response)) {
console.log("❌ login API call failed:", error?.response.data);
console.log("Server responded:", error?.response.data);
return { success: false, error: error?.response.data || "Request failed" };
} else {
return { success: false, error: error?.message || "Check you Internet connection and try again" };
}
}
}
} catch (err) {
if (err instanceof Error) {
return { success: false, error: err.message };
}
if (typeof err === "object" && err !== null && "response" in err) {
const axiosErr = err as any;
return { success: false, error: axiosErr.response?.data?.message || "Unknown error" };
}
return { success: false, error: "Unexpected error occurred" };
}
});
function handleCloseEvents(mainWindow: BrowserWindow) {
let willClose = false;

View File

@ -3,6 +3,8 @@ import { contextBridge, ipcRenderer } from "electron";
const electron = require('electron');
console.log("setting PreLoad")
contextBridge.exposeInMainWorld('electron', {
subscribeStatistics: (callback) =>
ipcOn('statistics', (stats) => {
@ -40,3 +42,10 @@ function ipcSend<Key extends keyof EventPayloadMapping>(
) {
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),
});

View File

@ -0,0 +1,21 @@
// electron/utils/tokenHelper.ts
import { safeStorage } from 'electron';
import Store from 'electron-store';
const store = new Store();
export async function getToken(TOKEN_KEY: string): Promise<string | null> {
const encryptedBase64 = store.get(TOKEN_KEY) as string | undefined;
if (!encryptedBase64) return null;
const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
return safeStorage.decryptString(encryptedBuffer);
}
export async function saveToken(TOKEN_KEY: string, token: string): Promise<void> {
const encryptedBuffer = safeStorage.encryptString(token);
store.set(TOKEN_KEY, encryptedBuffer.toString('base64'));
}
export async function clearToken(TOKEN_KEY: string): Promise<void> {
store.delete(TOKEN_KEY);
}

View File

@ -0,0 +1,92 @@
import { desktopCapturer } from "electron";
import * as fs from "fs";
import path from "path";
import axios from 'axios';
import sharp from 'sharp';
import { saveToken } from "./tokenHelper.js";
// Blur using an offscreen canvas
async function blurAndCompressBase64Image(base64: string): Promise<Buffer> {
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
const outputBuffer = await sharp(buffer)
.blur(8) // adjust blur radius here
.jpeg({ quality: 70 }) // compress output
.toBuffer();
return outputBuffer;
}
export const takeScreenshot = async () => {
// const sources = await desktopCapturer.getSources({ types: ["screen"] });
const sources = await desktopCapturer.getSources({
types: ['window'], thumbnailSize: {
height: 768,
width: 1366
}
});
// Capture the first screen (you can choose others by filtering sources)
const screen = sources[0];
let image = screen.thumbnail.toDataURL();
console.log("blurAndCompressBase64Image Calling")
const blurredBuffer = await blurAndCompressBase64Image(image);
const folderPath = "C:\\Users\\Public\\vatracker\\screenshots";
// Ensure directory exists
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`);
fs.writeFileSync(filePath, blurredBuffer);
console.log(`Screenshot saved at: ${filePath}`);
}
export const handleLogin = async (credentials: { email: string, password: string }) => {
try {
// Call your login API
console.log("🟡 login Request received", credentials);
try {
console.log("➡️ Calling API...");
const response = await axios.post(
'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login',
{ userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 } }
);
console.log("API CAlled", response)
const { accessToken, refreshToken } = response.data;
await saveToken(accessToken, refreshToken);
return { success: true, user: response.data.user };
} catch (error) {
if (error instanceof Error) {
if ("response" in error && typeof (error.response) == "object" && error.response !== null && ("data" in error.response)) {
console.log("❌ login API call failed:", error?.response.data);
console.log("Server responded:", error?.response.data);
return { success: false, error: error?.response.data || "Request failed" };
} else {
return { success: false, error: error?.message || "Check you Internet connection and try again" };
}
}
}
} catch (err) {
if (err instanceof Error) {
return { success: false, error: err.message };
}
if (typeof err === "object" && err !== null && "response" in err) {
const axiosErr = err as any;
return { success: false, error: axiosErr.response?.data?.message || "Unknown error" };
}
return { success: false, error: "Unexpected error occurred" };
}
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
@ -12,22 +12,23 @@ import Timesheets from "./pages/Timesheets/Timesheets";
import Settings from "./pages/Settings/Settings";
import HelpSupport from "./pages/HelpSupport/HelpSupport";
// ✅ Define User type (customize based on your structure)
interface User {
email: string;
password: string;
}
const App: React.FC = () => {
const theme = ThemeToggle();
const [user, setUser] = useState<User | null>(null);
// ✅ Apply theme
// Apply theme
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
// ✅ Load user from localStorage
// Load user from localStorage
useEffect(() => {
const storedUser = localStorage.getItem("user");
if (storedUser) {
@ -39,6 +40,8 @@ const App: React.FC = () => {
}
}, []);
return (
<BrowserRouter>
<Routes>
@ -65,8 +68,8 @@ const App: React.FC = () => {
<Route path="projects" element={<Projects />} />
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="timesheet" element={<Timesheets />} />
<Route path="settings" element={<Settings/>} />
<Route path="help-support" element={<HelpSupport/>} />
<Route path="settings" element={<Settings />} />
<Route path="help-support" element={<HelpSupport />} />
</Route>
{/* Default Redirect */}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import {
Outlet,
NavLink,
@ -25,9 +25,41 @@ interface User {
password: string;
}
interface Project {
id: number;
projectName: string;
clientName: string;
}
interface ProjectTrackingData {
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
isTracking: boolean;
isPaused: boolean;
}
interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<number, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (id: number) => void;
projectsData: Project[];
}
interface DashboardLayoutProps {
user: User | null;
setUser: React.Dispatch<React.SetStateAction<User | null>>;
// trackingState: TrackingState;
}
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
@ -36,6 +68,26 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const navigate = useNavigate();
const location = useLocation();
// Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
const [startTime, setStartTime] = useState<Date | null>(null);
const [endTime, setEndTime] = useState<Date | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [projectsData, setProjectsData] = useState<Project[]>([
{ id: 1, projectName: "CRM Redesign", clientName: "Salesforce" },
{ id: 2, projectName: "AI Assistant", clientName: "OpenAI" },
{ id: 3, projectName: "Marketing Dashboard", clientName: "Google" },
]);
const [projectTrackingMap, setProjectTrackingMap] = useState<
Map<number, ProjectTrackingData>
>(new Map());
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
@ -69,6 +121,180 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
];
// Sync global states with selected project tracking data
useEffect(() => {
if (selectedProject) {
const projectData = projectTrackingMap.get(selectedProject.id);
if (projectData) {
setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused);
setElapsedTime(projectData.elapsedTime);
setStartTime(projectData.startTime);
setEndTime(projectData.endTime);
}
}
}, [selectedProject, projectTrackingMap]);
// Start tracking
const handleStartTracking = (project: Project) => {
// Clear any existing timer
if (timerRef.current) {
clearInterval(timerRef.current);
}
// Update project tracking map
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(project.id) || {
elapsedTime: 0,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
};
const now = new Date();
current.startTime = now;
current.isTracking = true;
current.isPaused = false;
updated.set(project.id, current);
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
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 newPausedState = !currentProjectData.isPaused;
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
if (!current) return updated;
// Set the new pause state
current.isPaused = newPausedState;
updated.set(selectedProject.id, { ...current });
return updated;
});
// Update global isPaused state immediately
setIsPaused(newPausedState);
};
// Stop tracking
const handleStopTracking = () => {
if (!selectedProject) return;
// Clear the timer
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// Update project tracking map
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
if (!current) return updated;
const now = new Date();
current.isTracking = false;
current.isPaused = false;
current.endTime = now;
updated.set(selectedProject.id, { ...current });
return updated;
});
// Reset global tracking states to allow selecting another project
setIsTracking(false);
setIsPaused(false);
setEndTime(new Date());
console.log("projectTrackingMap",projectTrackingMap)
};
// Cleanup timer on unmount
useEffect(() => {
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
// 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);
setStartTime(projectData.startTime);
setEndTime(projectData.endTime);
setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused);
} else {
// Reset states for new project
setElapsedTime(0);
setStartTime(null);
setEndTime(null);
setIsTracking(false);
setIsPaused(false);
}
}
};
const trackingState: TrackingState = {
selectedProject,
projectTrackingMap,
isTracking,
isPaused,
elapsedTime,
startTime,
endTime,
handleStartTracking,
handlePauseResume,
handleStopTracking,
projectsData,
handleProjectSelect,
};
return (
<div className="flex h-screen bg-base-200 overflow-hidden">
{/* Sidebar */}
@ -192,7 +418,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
{/* Page Content */}
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
<Outlet />
<Outlet context={trackingState}/>
</main>
</div>
</div>

View File

@ -1,347 +1,161 @@
import React, { useState, useEffect, useRef } from "react";
import React from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import { useOutletContext } from "react-router-dom";
const projectsData = [
{
id: 1,
projectName: "Employee Tracker",
clientName: "TechCorp Pvt Ltd",
status: "Active",
action: "Start Tracking",
},
{
id: 2,
projectName: "Sales Dashboard",
clientName: "BlueEdge Analytics",
status: "Pending",
action: "Start Tracking",
},
{
id: 3,
projectName: "Inventory System",
clientName: "LogiFlow Inc",
status: "Completed",
action: "View Report",
},
{
id: 4,
projectName: "CRM Upgrade",
clientName: "NextWave Solutions",
status: "On Hold",
action: "Resume",
},
{
id: 5,
projectName: "Mobile App Redesign",
clientName: "PixelFusion",
status: "Active",
action: "Start Tracking",
},
{
id: 6,
projectName: "E-Commerce Backend",
clientName: "ShopEasy",
status: "Pending",
action: "Start Tracking",
},
{
id: 7,
projectName: "Finance Tracker",
clientName: "MoneyWise",
status: "Active",
action: "Start Tracking",
},
{
id: 8,
projectName: "Helpdesk Portal",
clientName: "ServePro",
status: "Completed",
action: "View Report",
},
{
id: 9,
projectName: "Marketing Automation",
clientName: "AdBoost",
status: "Active",
action: "Start Tracking",
},
{
id: 10,
projectName: "Data Pipeline",
clientName: "CloudWave",
status: "On Hold",
action: "Resume",
},
{
id: 11,
projectName: "Support Chatbot",
clientName: "SmartServe",
status: "Active",
action: "Start Tracking",
},
{
id: 12,
projectName: "Payroll System",
clientName: "PeopleFirst HR",
status: "Pending",
action: "Start Tracking",
},
{
id: 13,
projectName: "IoT Dashboard",
clientName: "SensorGrid",
status: "Completed",
action: "View Report",
},
{
id: 14,
projectName: "Security Portal",
clientName: "SafeNet",
status: "Active",
action: "Start Tracking",
},
{
id: 15,
projectName: "Social Media Tool",
clientName: "Buzzify",
status: "Pending",
action: "Start Tracking",
},
{
id: 16,
projectName: "AI Model Trainer",
clientName: "NeuroTech",
status: "On Hold",
action: "Resume",
},
{
id: 17,
projectName: "Project Tracker",
clientName: "TaskFlow",
status: "Active",
action: "Start Tracking",
},
{
id: 18,
projectName: "Booking Portal",
clientName: "GoTravel",
status: "Completed",
action: "View Report",
},
{
id: 19,
projectName: "Time Logger",
clientName: "Clockify Pro",
status: "Active",
action: "Start Tracking",
},
{
id: 20,
projectName: "Analytics Engine",
clientName: "DataStream",
status: "Pending",
action: "Start Tracking",
},
];
interface WidgetProps {
title: string;
value: string | number;
subtitle?: string;
icon?: React.ReactNode;
trend?: number;
color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info";
}
// ----- Interfaces -----
interface Project {
id: number;
projectName: string;
clientName: string;
}
// Widget Component
const Widget: React.FC<WidgetProps> = ({
title,
value,
subtitle,
icon,
trend,
color = "primary",
}) => {
return (
interface ProjectTrackingData {
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
isTracking: boolean;
isPaused: boolean;
}
interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<number, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (id: number) => void;
projectsData: Project[];
}
// ----- Helper -----
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`;
};
// ----- Widget -----
const Widget: React.FC<{
title: string;
value: string | number;
subtitle?: string;
icon?: React.ReactNode;
color?:
| "primary"
| "secondary"
| "accent"
| "success"
| "warning"
| "error"
| "info";
}> = ({ title, value, subtitle, icon, color = "primary" }) => (
<div className="card bg-base-100 shadow-sm hover:shadow-lg transition-shadow">
<div className="card-body">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className=" font-medium text-base-content/60">{title}</h3>
<div>
<h3 className="font-medium text-base-content/60">{title}</h3>
<p className="text-sm font-bold mt-2">{value}</p>
</div>
<div className={`p-3 bg-${color}/10 rounded-lg`}>
<div className={`text-${color}`}>{icon}</div>
</div>
</div>
{subtitle && <p className="text-xs text-base-content/60 mt-2">{subtitle}</p>}
</div>
</div>
);
};
);
// Main Dashboard Component
// ----- Dashboard -----
const Dashboard: React.FC = () => {
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0); // in seconds
const [startTime, setStartTime] = useState<Date | null>(null);
const [endTime, setEndTime] = useState<Date | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const {
selectedProject,
projectTrackingMap,
isTracking,
isPaused,
elapsedTime,
startTime,
handleStartTracking,
handlePauseResume,
handleStopTracking,
handleProjectSelect,
projectsData,
} = useOutletContext<TrackingState>();
// Load cached project or auto-select single project
useEffect(() => {
const cachedProject = localStorage.getItem("activeProject");
if (cachedProject) {
const parsed = JSON.parse(cachedProject) as Project;
setSelectedProject(parsed);
} else if (projectsData.length === 1) {
setSelectedProject(projectsData[0]);
}
}, []);
// Format elapsed time (hh:mm:ss)
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h}h ${m}m ${s}s`;
};
// Start tracking manually
const handleStartTracking = () => {
if (!selectedProject) return;
setIsTracking(true);
setIsPaused(false);
setElapsedTime(0);
setStartTime(new Date());
setEndTime(null);
// Cache selected project
localStorage.setItem("activeProject", JSON.stringify(selectedProject));
};
// ⏱ Timer logic
useEffect(() => {
if (isTracking && !isPaused) {
timerRef.current = setInterval(() => {
setElapsedTime((prev) => prev + 1);
}, 1000);
} else if (timerRef.current) {
clearInterval(timerRef.current);
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isTracking, isPaused]);
// ⏸ Pause/Resume button handler
const handlePauseResume = () => {
setIsPaused((prev) => !prev);
};
// Stop tracking
const handleStopTracking = () => {
if (!selectedProject) return;
setIsTracking(false);
setIsPaused(false);
setEndTime(new Date());
if (timerRef.current) clearInterval(timerRef.current);
// Keep project in view, just stop tracking.
// Remove tracking state but keep the selected project cached.
const summary = {
project: selectedProject.projectName,
totalTime: formatTime(elapsedTime),
start: startTime?.toLocaleTimeString(),
end: new Date().toLocaleTimeString(),
};
console.log("✅ Final Tracking Summary:", summary);
// Save the stopped state for reference
localStorage.setItem(
"activeProject",
JSON.stringify({
...selectedProject,
lastSession: summary,
})
);
// Reset timer values but keep selected project
setElapsedTime(0);
};
// 🧾 When user picks a project manually
const handleProjectSelect = (id: number) => {
if (isTracking) return;
const project = projectsData.find((item) => item.id === id);
if (project) {
setSelectedProject(project);
localStorage.setItem("activeProject", JSON.stringify(project));
}
};
const selectedData = selectedProject
? projectTrackingMap.get(selectedProject.id)
: null;
return (
<main className="flex-1 overflow-y-auto md:px-0.5 md:py-0 no-scrollbar">
{/* Welcome Section */}
<div className="mb-4">
<h1 className="text-lg font-bold">Dashboard</h1>
<main className="flex-1 overflow-y-auto no-scrollbar">
{/* Header */}
<div className="mb-2">
<h1 className="text-md font-bold">Dashboard</h1>
<button
onClick={async () => {
await window.electron.uploadScreenshot();
}}
className="btn btn-xs btn-outline mt-2"
>
ScreenShot Test
Screenshot Test
</button>
</div>
{/* Widgets Grid */}
<div className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4 ">
{/* Widgets */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Widget
title="Logged Hours Today"
value="6h 24m"
subtitle="Today's total time"
icon={<Clock size={24} />}
trend={12}
color="primary"
/>
<Widget
title="Logged Hours This Week"
value="4h 48m"
subtitle="75% productivity"
icon={<TrendingUp size={24} />}
trend={8}
color="success"
/>
<Widget
title="Active Project"
value={selectedProject ? selectedProject.projectName : "—"}
subtitle="Until day ends"
icon={<ProjectorIcon size={24} />}
color="warning"
/>
</div>
{/* Tracking Section */}
<div className="w-full mb-4">
<div className="flex justify-between items-center mb-2">
<span className="text-md font-bold">Tracking</span>
</div>
{/* Tracking Card */}
<div className="w-full mb-4">
<div className="text-md mb-2 font-bold flex justify-between items-center">
<span>Tracking</span>
{isTracking && (
<div className="card bg-base-100 border border-base-200 shadow-sm">
<div className="card-body">
{selectedProject ? (
<>
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-center sm:text-left">
<p className="text-lg font-extrabold text-success">
{formatTime(selectedData?.elapsedTime || 0)}
</p>
<p className="text-xs text-base-content/70 mt-1">
{selectedData?.isTracking ? "Current Session" : "Ready"}
</p>
</div>
{!selectedData?.isTracking ? (
<button
onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
>
<Play className="w-3 h-3" /> Start Tracking
</button>
) : (
<button
className="btn btn-xs btn-error rounded-full"
onClick={handleStopTracking}
@ -352,92 +166,49 @@ const Dashboard: React.FC = () => {
)}
</div>
<div className="card bg-base-100 shadow-sm border border-base-200 transition-all duration-300 w-full">
<div className="card-body">
{selectedProject ? (
<>
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-center sm:text-left">
<p className="text-sm font-extrabold text-success tracking-tight">
{formatTime(elapsedTime)}
</p>
<p className="text-xs text-base-content/70 mt-1">
{isTracking ? "Current Session" : "Ready to Start"}
</p>
</div>
{!isTracking ? (
<button
onClick={handleStartTracking}
className="btn btn-sm btn-success rounded-xl hover:scale-105 transition-transform duration-200 w-full sm:w-auto"
>
<Play className="w-3 h-3" />
Start Tracking
</button>
) : (
<button
onClick={handlePauseResume}
className={`btn btn-sm rounded-xl transition-transform duration-200 w-full sm:w-auto ${isPaused ? "btn-primary" : "btn-warning"
}`}
>
{isPaused ? (
<>
<Play className="w-3 h-3" />
Resume
</>
) : (
<>
<Pause className="w-3 h-3" />
Pause
</>
)}
</button>
)}
</div>
<div className="divider my-2"></div>
<div className="divider my-3" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
{/* Project Name */}
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
<span className="text-base-content/70">Project:</span>
<span className="font-semibold text-base-content truncate">
<div>
<span className="text-base-content/70">Project:</span>{" "}
<span className="font-semibold">
{selectedProject.projectName}
</span>
</div>
{/* Client Name */}
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
<span className="text-base-content/70">Client:</span>
<span className="truncate">{selectedProject.clientName}</span>
<div>
<span className="text-base-content/70">Client:</span>{" "}
<span>{selectedProject.clientName}</span>
</div>
{/* Status */}
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
<span className="text-base-content/70">Status:</span>
<div>
<span className="text-base-content/70">Status:</span>{" "}
<span
className={`badge ${isPaused
className={`badge ${selectedData?.isPaused
? "badge-warning"
: isTracking
: selectedData?.isTracking
? "badge-success"
: "badge-neutral"
}`}
>
{isPaused ? "Paused" : isTracking ? "Active" : "Ready"}
{selectedData?.isPaused
? "Paused"
: selectedData?.isTracking
? "Active"
: "Ready"}
</span>
</div>
{/* Start Time */}
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
<span className="text-base-content/70">Start Time:</span>
<span>{startTime?.toLocaleTimeString() || "—"}</span>
<div>
<span className="text-base-content/70">Start:</span>{" "}
<span>
{selectedData?.startTime
? selectedData.startTime.toLocaleTimeString()
: "—"}
</span>
</div>
</div>
</>
) : (
<p className="text-sm text-base-content/60 text-center">
Select a project to start tracking.
Select a project below to start tracking.
</p>
)}
</div>
@ -447,55 +218,44 @@ const Dashboard: React.FC = () => {
{/* Projects Table */}
<div>
<p className="text-md mb-2 font-bold">Projects</p>
<div
className="overflow-y-auto no-scrollbar overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
style={{ minHeight: "100px", maxHeight: "30vh" }}
>
<div className="overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md">
<table className="table table-zebra w-full text-sm">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Project Name</th>
<th>Client Name</th>
<th>Project</th>
<th>Client</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{projectsData.map((project) => (
<tr key={project.id} className="align-middle">
<th>{project.id}</th>
{projectsData.map((project) => {
const isCurrentlySelected = selectedProject?.id === project.id;
// ✅ FIX: Disable ALL projects when ANY project is tracking
// Only enable selection when not tracking
const isDisabled = isTracking && !isCurrentlySelected;
return (
<tr key={project.id}>
<td>{project.id}</td>
<td>{project.projectName}</td>
<td>{project.clientName}</td>
<td>
<div
className={`badge badge-sm whitespace-nowrap ${project.status === "Active"
? "badge-success"
: project.status === "Pending"
? "badge-warning"
: project.status === "Completed"
? "badge-info"
: "badge-neutral"
<button
className={`btn btn-xs ${isCurrentlySelected
? "btn-success"
: "btn-primary"
}`}
onClick={() => handleProjectSelect(project.id)}
disabled={isDisabled}
>
{project.status}
</div>
</td>
<td>
<div
className={`badge badge-sm whitespace-nowrap ${project.action === "View Report"
? "badge-info"
: project.action === "Resume"
? "badge-secondary"
: "badge-primary"
}`}
>
Select
{isCurrentlySelected ? "Selected" : "Select"}
</button>
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>
</div>

View File

@ -11,7 +11,7 @@ interface LoginProps {
}
const Login: React.FC<LoginProps> = ({ setUser }) => {
const [formData, setFormData] = useState<User>({ email: 'jai@gmail.com', password: '12345' })
const [formData, setFormData] = useState<User>({ email: '', password: '' })
const inputRef = useRef<HTMLFormElement | null>(null)
const navigate = useNavigate()