From 70b3e412adde3f0b68ead81549fff2b340d356ce Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sat, 24 May 2025 12:51:57 +0530 Subject: [PATCH] feat: implement navigation context and enhance component loading states across the application --- src/components/activity/ActivityLog.tsx | 10 ++- src/components/config/ConfigTabs.tsx | 5 ++ src/components/dashboard/Dashboard.tsx | 12 ++- src/components/layout/Header.tsx | 15 +++- src/components/layout/MainLayout.tsx | 86 +++++++++++++++---- src/components/layout/Sidebar.tsx | 44 ++++++++-- src/components/organizations/Organization.tsx | 10 ++- src/components/repositories/Repository.tsx | 12 ++- src/hooks/useConfigStatus.ts | 83 +++++++++++++++--- 9 files changed, 229 insertions(+), 48 deletions(-) diff --git a/src/components/activity/ActivityLog.tsx b/src/components/activity/ActivityLog.tsx index 380ae4b..09be2fc 100644 --- a/src/components/activity/ActivityLog.tsx +++ b/src/components/activity/ActivityLog.tsx @@ -26,6 +26,7 @@ import { useFilterParams } from '@/hooks/useFilterParams'; import { toast } from 'sonner'; import { useLiveRefresh } from '@/hooks/useLiveRefresh'; import { useConfigStatus } from '@/hooks/useConfigStatus'; +import { useNavigation } from '@/components/layout/MainLayout'; type MirrorJobWithKey = MirrorJob & { _rowKey: string }; @@ -41,6 +42,7 @@ export function ActivityLog() { const { user } = useAuth(); const { registerRefreshCallback } = useLiveRefresh(); const { isFullyConfigured } = useConfigStatus(); + const { navigationKey } = useNavigation(); const [activities, setActivities] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -71,7 +73,7 @@ export function ActivityLog() { /* ------------------------- initial fetch --------------------------- */ const fetchActivities = useCallback(async () => { - if (!user) return false; + if (!user?.id) return false; try { setIsLoading(true); @@ -101,11 +103,13 @@ export function ActivityLog() { } finally { setIsLoading(false); } - }, [user]); + }, [user?.id]); // Only depend on user.id, not entire user object useEffect(() => { + // Reset loading state when component becomes active + setIsLoading(true); fetchActivities(); - }, [fetchActivities]); + }, [fetchActivities, navigationKey]); // Include navigationKey to trigger on navigation // Register with global live refresh system useEffect(() => { diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index decca2b..7cb5902 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -16,6 +16,7 @@ import { apiRequest } from '@/lib/utils'; import { RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { Skeleton } from '@/components/ui/skeleton'; +import { invalidateConfigCache } from '@/hooks/useConfigStatus'; type ConfigState = { githubConfig: GitHubConfig; @@ -117,6 +118,8 @@ export function ConfigTabs() { if (result.success) { await refreshUser(); setIsConfigSaved(true); + // Invalidate config cache so other components get fresh data + invalidateConfigCache(); toast.success( 'Configuration saved successfully! Now import your GitHub data to begin.', ); @@ -165,6 +168,8 @@ export function ConfigTabs() { if (result.success) { // Silent success - no toast for auto-save // Removed refreshUser() call to prevent page reload + // Invalidate config cache so other components get fresh data + invalidateConfigCache(); } else { toast.error( `Auto-save failed: ${result.message || 'Unknown error'}`, diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index 1115c7d..b3aa7bb 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -14,12 +14,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useLiveRefresh } from "@/hooks/useLiveRefresh"; import { usePageVisibility } from "@/hooks/usePageVisibility"; import { useConfigStatus } from "@/hooks/useConfigStatus"; +import { useNavigation } from "@/components/layout/MainLayout"; export function Dashboard() { const { user } = useAuth(); const { registerRefreshCallback } = useLiveRefresh(); const isPageVisible = usePageVisibility(); const { isFullyConfigured } = useConfigStatus(); + const { navigationKey } = useNavigation(); const [repositories, setRepositories] = useState([]); const [organizations, setOrganizations] = useState([]); @@ -68,7 +70,7 @@ export function Dashboard() { // Extract fetchDashboardData as a stable callback const fetchDashboardData = useCallback(async (showToast = false) => { try { - if (!user || !user.id) { + if (!user?.id) { return false; } @@ -114,12 +116,14 @@ export function Dashboard() { } finally { setIsLoading(false); } - }, [user, isFullyConfigured]); + }, [user?.id, isFullyConfigured]); // Only depend on user.id, not entire user object - // Initial data fetch + // Initial data fetch and reset loading state when component becomes active useEffect(() => { + // Reset loading state when component mounts or becomes active + setIsLoading(true); fetchDashboardData(); - }, [fetchDashboardData]); + }, [fetchDashboardData, navigationKey]); // Include navigationKey to trigger on navigation // Setup dashboard auto-refresh (30 seconds) and register with live refresh useEffect(() => { diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 92ee7a6..10c8e55 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -10,9 +10,10 @@ import { useConfigStatus } from "@/hooks/useConfigStatus"; interface HeaderProps { currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log"; + onNavigate?: (page: string) => void; } -export function Header({ currentPage }: HeaderProps) { +export function Header({ currentPage, onNavigate }: HeaderProps) { const { user, logout, isLoading } = useAuth(); const { isLiveEnabled, toggleLive } = useLiveRefresh(); const { isFullyConfigured, isLoading: configLoading } = useConfigStatus(); @@ -49,10 +50,18 @@ export function Header({ currentPage }: HeaderProps) { return (
- +
{showLiveButton && ( diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 6b1f34b..19463a2 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect, createContext, useContext } from "react"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; import { Dashboard } from "@/components/dashboard/Dashboard"; @@ -9,6 +10,12 @@ import { Organization } from "../organizations/Organization"; import { Toaster } from "@/components/ui/sonner"; import { useAuth } from "@/hooks/useAuth"; import { useRepoSync } from "@/hooks/useSyncRepo"; +import { useConfigStatus } from "@/hooks/useConfigStatus"; + +// Navigation context to signal when navigation happens +const NavigationContext = createContext<{ navigationKey: number }>({ navigationKey: 0 }); + +export const useNavigation = () => useContext(NavigationContext); interface AppProps { page: @@ -32,8 +39,12 @@ export default function App({ page }: AppProps) { ); } -function AppWithProviders({ page }: AppProps) { - const { user } = useAuth(); +function AppWithProviders({ page: initialPage }: AppProps) { + const { user, isLoading: authLoading } = useAuth(); + const { isLoading: configLoading } = useConfigStatus(); + const [currentPage, setCurrentPage] = useState(initialPage); + const [navigationKey, setNavigationKey] = useState(0); + useRepoSync({ userId: user?.id, enabled: user?.syncEnabled, @@ -42,20 +53,63 @@ function AppWithProviders({ page }: AppProps) { nextSync: user?.nextSync, }); + // Handle navigation from sidebar + const handleNavigation = (pageName: string) => { + setCurrentPage(pageName as AppProps['page']); + // Increment navigation key to force components to refresh their loading state + setNavigationKey(prev => prev + 1); + }; + + // Handle browser back/forward navigation + useEffect(() => { + const handlePopState = () => { + const path = window.location.pathname; + const pageMap: Record = { + '/': 'dashboard', + '/repositories': 'repositories', + '/organizations': 'organizations', + '/config': 'configuration', + '/activity': 'activity-log' + }; + + const pageName = pageMap[path] || 'dashboard'; + setCurrentPage(pageName); + }; + + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, []); + + // Show loading state only during initial auth/config loading + const isInitialLoading = authLoading || (configLoading && !user); + + if (isInitialLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + return ( -
-
-
- -
- {page === "dashboard" && } - {page === "repositories" && } - {page === "organizations" && } - {page === "configuration" && } - {page === "activity-log" && } -
-
- -
+ +
+
+
+ +
+ {currentPage === "dashboard" && } + {currentPage === "repositories" && } + {currentPage === "organizations" && } + {currentPage === "configuration" && } + {currentPage === "activity-log" && } +
+
+ +
+
); } diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 43d0626..8ac6ac9 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -6,9 +6,10 @@ import { VersionInfo } from "./VersionInfo"; interface SidebarProps { className?: string; + onNavigate?: (page: string) => void; } -export function Sidebar({ className }: SidebarProps) { +export function Sidebar({ className, onNavigate }: SidebarProps) { const [currentPath, setCurrentPath] = useState(""); useEffect(() => { @@ -18,6 +19,39 @@ export function Sidebar({ className }: SidebarProps) { console.log("Hydrated path:", path); // Should log now }, []); + // Listen for URL changes (browser back/forward) + useEffect(() => { + const handlePopState = () => { + setCurrentPath(window.location.pathname); + }; + + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, []); + + const handleNavigation = (href: string, event: React.MouseEvent) => { + event.preventDefault(); + + // Don't navigate if already on the same page + if (currentPath === href) return; + + // Update URL without page reload + window.history.pushState({}, '', href); + setCurrentPath(href); + + // Map href to page name for the parent component + const pageMap: Record = { + '/': 'dashboard', + '/repositories': 'repositories', + '/organizations': 'organizations', + '/config': 'configuration', + '/activity': 'activity-log' + }; + + const pageName = pageMap[href] || 'dashboard'; + onNavigate?.(pageName); + }; + return (