diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 936a8cc..4fa04de 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.0", + "@types/lodash": "^4.17.24", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.0", @@ -1939,6 +1940,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2b0d12f..65dde83 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.0", + "@types/lodash": "^4.17.24", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.0", diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..ce6d7cd --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,9 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import TrackerPage from './components/tracker/TrackerPage'; +import StatsPage from './components/stats/StatsPage'; +import Navbar from './components/Navbar'; +function App() { + return (_jsx(Router, { children: _jsxs("div", { className: "min-h-screen bg-[#f8fafc] text-slate-900 font-sans antialiased", children: [_jsx(Navbar, {}), _jsx("main", { className: "container mx-auto px-4 sm:px-6 lg:px-8 mt-8", children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(TrackerPage, {}) }), _jsx(Route, { path: "/stats", element: _jsx(StatsPage, {}) })] }) })] }) })); +} +export default App; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8516480..dab3f75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,9 +7,9 @@ import Navbar from './components/Navbar'; function App() { return ( -
+
-
+
} /> } /> diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..93836f4 --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,96 @@ +const BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; +export function localToUTC(localValue) { + return localValue; +} +export async function fetchActiveSession() { + const response = await fetch(`${BASE_URL}/api/sessions/active`); + if (!response.ok) + throw new Error('Failed to fetch active session'); + const data = await response.json(); + return data.session; +} +export async function fetchSessions(from_date, to_date, limit = 200) { + const params = new URLSearchParams(); + if (from_date) + params.append('from_date', from_date); + if (to_date) + params.append('to_date', to_date); + if (limit) + params.append('limit', limit.toString()); + const response = await fetch(`${BASE_URL}/api/sessions?${params.toString()}`); + if (!response.ok) + throw new Error('Failed to fetch sessions'); + return response.json(); +} +export async function createSession(payload) { + const utcTime = localToUTC(payload.start_time); + const response = await fetch(`${BASE_URL}/api/sessions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ start_time: utcTime }), + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Failed to create session'); + } + return response.json(); +} +export async function endSession(sessionId, payload) { + const utcTime = payload.end_time ? localToUTC(payload.end_time) : undefined; + const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/end`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ end_time: utcTime }), + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Failed to end session'); + } + return response.json(); +} +export async function updateSession(sessionId, payload) { + const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Failed to update session'); + } + return response.json(); +} +export async function deleteSession(sessionId) { + const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}`, { + method: 'DELETE', + }); + if (!response.ok) + throw new Error('Failed to delete session'); +} +export async function fetchDailySummary(from_date, to_date, day_start, day_end) { + const fromDT = from_date + 'T00:00:00'; + const toDT = to_date + 'T23:59:59'; + const params = new URLSearchParams({ from_date: fromDT, to_date: toDT, day_start, day_end }); + const response = await fetch(`${BASE_URL}/api/stats/daily-summary?${params.toString()}`); + if (!response.ok) + throw new Error('Failed to fetch daily summary'); + return response.json(); +} +export async function fetchTimetable(from_date, to_date, day_start, day_end) { + const fromDT = from_date + 'T00:00:00'; + const toDT = to_date + 'T23:59:59'; + const params = new URLSearchParams({ from_date: fromDT, to_date: toDT, day_start, day_end }); + const response = await fetch(`${BASE_URL}/api/stats/timetable?${params.toString()}`); + if (!response.ok) + throw new Error('Failed to fetch timetable'); + return response.json(); +} +export async function fetchWakeSleepTimes(from_date, to_date, wakeup_ref, bedtime_ref) { + const fromDT = from_date + 'T00:00:00'; + const toDT = to_date + 'T23:59:59'; + const params = new URLSearchParams({ from_date: fromDT, to_date: toDT, wakeup_ref, bedtime_ref }); + const response = await fetch(`${BASE_URL}/api/stats/wake-sleep-times?${params.toString()}`); + if (!response.ok) + throw new Error('Failed to fetch wake/sleep times'); + return response.json(); +} diff --git a/frontend/src/components/ErrorBoundary.js b/frontend/src/components/ErrorBoundary.js new file mode 100644 index 0000000..59afd53 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.js @@ -0,0 +1,20 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Component } from 'react'; +export class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + componentDidCatch(error, errorInfo) { + console.error("ErrorBoundary caught an error", error, errorInfo.componentStack); + } + render() { + if (this.state.hasError) { + return (this.props.fallback || (_jsxs("div", { className: "bg-red-50 border border-red-200 rounded-lg p-4 text-center", children: [_jsx("p", { className: "text-red-600 font-medium", children: "Failed to load this component" }), _jsx("button", { onClick: () => this.setState({ hasError: false }), className: "mt-2 text-sm text-red-500 hover:underline", children: "Try again" })] }))); + } + return this.props.children; + } +} diff --git a/frontend/src/components/Navbar.js b/frontend/src/components/Navbar.js new file mode 100644 index 0000000..5d7c652 --- /dev/null +++ b/frontend/src/components/Navbar.js @@ -0,0 +1,9 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Link, useLocation } from 'react-router-dom'; +const Navbar = () => { + const location = useLocation(); + // Helper to check if a path is active + const isActive = (path) => location.pathname === path; + return (_jsx("nav", { className: "bg-white/80 backdrop-blur-md sticky top-0 z-50 border-b border-gray-100", children: _jsx("div", { className: "max-w-md mx-auto px-4 flex justify-center items-center h-16", children: _jsxs("div", { className: "flex space-x-2 bg-gray-100 p-1 rounded-xl", children: [_jsx(Link, { to: "/", className: `px-6 py-2 rounded-lg text-sm font-bold transition-all duration-200 ${isActive('/') ? 'bg-white text-indigo-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`, children: "Tracker" }), _jsx(Link, { to: "/stats", className: `px-6 py-2 rounded-lg text-sm font-bold transition-all duration-200 ${isActive('/stats') ? 'bg-white text-indigo-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`, children: "Stats" })] }) }) })); +}; +export default Navbar; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index e028088..c8b9737 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -8,23 +8,27 @@ const Navbar: React.FC = () => { const isActive = (path: string) => location.pathname === path; return ( -