diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 01870ac..aae73f7 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Outlet } from 'react-router-dom'; import { Menu } from 'lucide-react'; import { useTheme } from '@/hooks/useTheme'; +import { usePrefetch } from '@/hooks/usePrefetch'; import { AlertsProvider } from '@/hooks/useAlerts'; import { LockProvider, useLock } from '@/hooks/useLock'; import { NotificationProvider } from '@/hooks/useNotifications'; @@ -16,6 +17,7 @@ function AppContent({ mobileOpen, setMobileOpen }: { setMobileOpen: (v: boolean) => void; }) { const { isLocked, isLockResolved } = useLock(); + usePrefetch(isLockResolved && !isLocked); const [collapsed, setCollapsed] = useState(() => { try { return JSON.parse(localStorage.getItem('umbra-sidebar-collapsed') || 'false'); } catch { return false; } diff --git a/frontend/src/hooks/usePrefetch.ts b/frontend/src/hooks/usePrefetch.ts new file mode 100644 index 0000000..96dbf60 --- /dev/null +++ b/frontend/src/hooks/usePrefetch.ts @@ -0,0 +1,62 @@ +import { useEffect, useRef } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import api from '@/lib/api'; +import { useSettings } from './useSettings'; + +/** + * Prefetches main page data in the background after the app unlocks. + * Ensures cache is warm before the user navigates to each tab, + * eliminating the skeleton flash on first visit. + */ +export function usePrefetch(enabled: boolean) { + const queryClient = useQueryClient(); + const { settings } = useSettings(); + const hasPrefetched = useRef(false); + + useEffect(() => { + if (!enabled || hasPrefetched.current) return; + hasPrefetched.current = true; + + const now = new Date(); + const clientDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + const days = settings?.upcoming_days || 7; + + // Prefetch all main page queries (no-op if already cached and fresh) + queryClient.prefetchQuery({ + queryKey: ['dashboard'], + queryFn: () => api.get(`/dashboard?client_date=${clientDate}`).then(r => r.data), + staleTime: 60_000, + }); + + queryClient.prefetchQuery({ + queryKey: ['upcoming', days], + queryFn: () => api.get(`/upcoming?days=${days}&client_date=${clientDate}`).then(r => r.data), + staleTime: 60_000, + }); + + queryClient.prefetchQuery({ + queryKey: ['todos'], + queryFn: () => api.get('/todos').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['reminders'], + queryFn: () => api.get('/reminders').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['projects'], + queryFn: () => api.get('/projects').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['people'], + queryFn: () => api.get('/people').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['locations'], + queryFn: () => api.get('/locations').then(r => r.data), + }); + }, [enabled, queryClient, settings?.upcoming_days]); +}