diff --git a/client/App.jsx b/client/App.jsx index 06ecfff..89e25d1 100644 --- a/client/App.jsx +++ b/client/App.jsx @@ -9,6 +9,20 @@ import LoginPage from '@/pages/LoginPage'; import ErrorBoundary from '@/components/ErrorBoundary'; import PageLoader from '@/components/PageLoader'; +// TanStack Query +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 2, // 2 minutes + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + // Lazy-loaded components const AdminPage = lazy(() => import('@/pages/AdminPage')); const TrackerPage = lazy(() => import('@/pages/TrackerPage')); @@ -75,7 +89,7 @@ export default function App() { const mainContentId = useId(); return ( - <> + {/* Release notes (only for user role) */} {user?.role === 'user' && } @@ -176,6 +190,7 @@ export default function App() { - + + ); } diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js new file mode 100644 index 0000000..92a254f --- /dev/null +++ b/client/hooks/useQueries.js @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/api'; + +// Custom hook for fetching tracker data +export function useTracker(year, month) { + return useQuery({ + queryKey: ['tracker', year, month], + queryFn: () => api.tracker(year, month), + staleTime: 1000 * 60 * 5, // 5 minutes + cacheTime: 1000 * 60 * 30, // 30 minutes + }); +} + +// Custom hook for fetching all bills +export function useBills() { + return useQuery({ + queryKey: ['bills'], + queryFn: () => api.allBills(), + staleTime: 1000 * 60 * 5, // 5 minutes + cacheTime: 1000 * 60 * 30, // 30 minutes + }); +} + +// Custom hook for fetching categories +export function useCategories() { + return useQuery({ + queryKey: ['categories'], + queryFn: () => api.categories(), + staleTime: 1000 * 60 * 60, // 1 hour + cacheTime: 1000 * 60 * 60 * 2, // 2 hours + }); +} \ No newline at end of file diff --git a/client/lib/version.js b/client/lib/version.js index 7ede401..fcfc02f 100644 --- a/client/lib/version.js +++ b/client/lib/version.js @@ -1,10 +1,11 @@ -export const APP_VERSION = '0.21.1'; +export const APP_VERSION = '0.22.0'; export const APP_NAME = 'BillTracker'; export const RELEASE_NOTES = { - version: '0.21.1', + version: '0.22.0', date: '2026-05-10', highlights: [ - { icon: '💫', title: 'Loading Skeletons', desc: 'Tracker and Bills pages show skeleton placeholders during data loading.' }, + { icon: '🔄', title: 'React Query Migration', desc: 'Tracker page now uses TanStack Query for data fetching with caching and auto-refetch.' }, + { icon: '⚡', title: 'Query DevTools', desc: 'React Query DevTools available in development for inspecting query state.' }, ], }; \ No newline at end of file diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 7e340d2..1338330 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { ChevronLeft, ChevronRight, Pencil, TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api.js'; +import { useTracker } from '@/hooks/useQueries'; import BillModal from '@/components/BillModal'; import { cn, fmt, fmtDate, todayStr } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -1361,26 +1362,13 @@ export default function TrackerPage() { const now = new Date(); const [year, setYear] = useState(now.getFullYear()); const [month, setMonth] = useState(now.getMonth() + 1); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); // Edit Bill modal: { bill, categories } when open, null when closed const [editBillData, setEditBillData] = useState(null); // Edit Starting Amounts modal: true when open, false when closed const [editStartingOpen, setEditStartingOpen] = useState(false); - const load = useCallback(async () => { - setLoading(true); - try { - const res = await api.tracker(year, month); - setData(res); - } catch (err) { - toast.error(err.message); - } finally { - setLoading(false); - } - }, [year, month]); - - useEffect(() => { load(); }, [load]); + // Use React Query for data fetching + const { data, isLoading: loading, isError, error, refetch } = useTracker(year, month); function navigate(delta) { setMonth(m => { @@ -1409,6 +1397,16 @@ export default function TrackerPage() { setMonth(n.getMonth() + 1); } + // Handle errors from React Query (use ref to prevent duplicate toasts) + const errorShownRef = useRef(false); + useEffect(() => { + if (isError && !errorShownRef.current) { + toast.error(error?.message || 'Failed to load tracker data'); + errorShownRef.current = true; + } + if (!isError) errorShownRef.current = false; + }, [isError, error]); + const rows = data?.rows || []; const summary = data?.summary || {}; const first = rows.filter(r => r.bucket === '1st'); @@ -1529,8 +1527,8 @@ export default function TrackerPage() { )} - {first.length > 0 && } - {second.length > 0 && } + {first.length > 0 && } + {second.length > 0 && } {/* Edit Bill modal — opened by clicking a bill name in any tracker row */} {editBillData && ( @@ -1538,7 +1536,7 @@ export default function TrackerPage() { bill={editBillData.bill} categories={editBillData.categories} onClose={() => setEditBillData(null)} - onSave={() => { setEditBillData(null); load(); }} + onSave={() => { setEditBillData(null); refetch(); }} /> )} @@ -1548,7 +1546,7 @@ export default function TrackerPage() { onClose={() => setEditStartingOpen(false)} year={year} month={month} - onSave={() => { setEditStartingOpen(false); load(); }} + onSave={() => { setEditStartingOpen(false); refetch(); }} /> diff --git a/package-lock.json b/package-lock.json index de81d3a..7d8ad07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bill-tracker", - "version": "0.19.0", + "version": "0.21.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bill-tracker", - "version": "0.19.0", + "version": "0.21.1", "license": "ISC", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.2", @@ -20,6 +20,8 @@ "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", + "@tanstack/react-query": "^5.100.9", + "@tanstack/react-query-devtools": "^5.100.9", "bcryptjs": "^2.4.3", "better-sqlite3": "^12.9.0", "class-variance-authority": "^0.7.0", @@ -2286,6 +2288,59 @@ "win32" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz", + "integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.9.tgz", + "integrity": "sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz", + "integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.9.tgz", + "integrity": "sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.100.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.100.9", + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/package.json b/package.json index d234dbb..ace09bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.21.1", + "version": "0.22.0", "description": "Monthly bill tracking system", "main": "server.js", "scripts": { @@ -22,6 +22,8 @@ "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", + "@tanstack/react-query": "^5.100.9", + "@tanstack/react-query-devtools": "^5.100.9", "bcryptjs": "^2.4.3", "better-sqlite3": "^12.9.0", "class-variance-authority": "^0.7.0",