import { StatusCard } from "./StatusCard"; import { RecentActivity } from "./RecentActivity"; import { RepositoryList } from "./RepositoryList"; import { GitFork, Clock, FlipHorizontal, Building2 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { MirrorJob, Organization, Repository } from "@/lib/db/schema"; import { useAuth } from "@/hooks/useAuth"; import { apiRequest, showErrorToast } from "@/lib/utils"; import type { DashboardApiResponse } from "@/types/dashboard"; import { useSSE } from "@/hooks/useSEE"; import { toast } from "sonner"; import { Skeleton } from "@/components/ui/skeleton"; 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([]); const [activities, setActivities] = useState([]); const [isLoading, setIsLoading] = useState(true); const [repoCount, setRepoCount] = useState(0); const [orgCount, setOrgCount] = useState(0); const [mirroredCount, setMirroredCount] = useState(0); const [lastSync, setLastSync] = useState(null); // Dashboard auto-refresh timer (30 seconds) const dashboardTimerRef = useRef(null); const DASHBOARD_REFRESH_INTERVAL = 30000; // 30 seconds // Create a stable callback using useCallback const handleNewMessage = useCallback((data: MirrorJob) => { if (data.repositoryId) { setRepositories((prevRepos) => prevRepos.map((repo) => repo.id === data.repositoryId ? { ...repo, status: data.status, details: data.details } : repo ) ); } else if (data.organizationId) { setOrganizations((prevOrgs) => prevOrgs.map((org) => org.id === data.organizationId ? { ...org, status: data.status, details: data.details } : org ) ); } setActivities((prevActivities) => [data, ...prevActivities]); console.log("Received new log:", data); }, []); // Use the SSE hook const { connected } = useSSE({ userId: user?.id, onMessage: handleNewMessage, }); // Extract fetchDashboardData as a stable callback const fetchDashboardData = useCallback(async (showToast = false) => { try { if (!user?.id) { return false; } // Don't fetch data if configuration is not complete if (!isFullyConfigured) { if (showToast) { toast.info("Please configure GitHub and Gitea settings first"); } return false; } const response = await apiRequest( `/dashboard?userId=${user.id}`, { method: "GET", } ); if (response.success) { setRepositories(response.repositories); setOrganizations(response.organizations); setActivities(response.activities); setRepoCount(response.repoCount); setOrgCount(response.orgCount); setMirroredCount(response.mirroredCount); setLastSync(response.lastSync); if (showToast) { toast.success("Dashboard data refreshed successfully"); } return true; } else { showErrorToast(response.error || "Error fetching dashboard data", toast); return false; } } catch (error) { showErrorToast(error, toast); return false; } finally { setIsLoading(false); } }, [user?.id, isFullyConfigured]); // Only depend on user.id, not entire user object // 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, navigationKey]); // Include navigationKey to trigger on navigation // Setup dashboard auto-refresh (30 seconds) and register with live refresh useEffect(() => { // Clear any existing timer if (dashboardTimerRef.current) { clearInterval(dashboardTimerRef.current); dashboardTimerRef.current = null; } // Set up 30-second auto-refresh only when page is visible and configuration is complete if (isPageVisible && isFullyConfigured) { dashboardTimerRef.current = setInterval(() => { fetchDashboardData(); }, DASHBOARD_REFRESH_INTERVAL); } // Cleanup on unmount or when page becomes invisible return () => { if (dashboardTimerRef.current) { clearInterval(dashboardTimerRef.current); dashboardTimerRef.current = null; } }; }, [isPageVisible, isFullyConfigured, fetchDashboardData]); // Register with global live refresh system useEffect(() => { // Only register if configuration is complete if (!isFullyConfigured) { return; } const unregister = registerRefreshCallback(() => { fetchDashboardData(); }); return unregister; }, [registerRefreshCallback, fetchDashboardData, isFullyConfigured]); // Status Card Skeleton component function StatusCardSkeleton() { return ( ); } return isLoading || !connected ? (
{/* Repository List Skeleton */}
{Array.from({ length: 3 }).map((_, i) => ( ))}
{/* Recent Activity Skeleton */}
{Array.from({ length: 3 }).map((_, i) => ( ))}
) : (
} description="Repositories being mirrored" /> } description="Successfully mirrored" /> } description="GitHub organizations" /> } description="Last successful sync" />
{/* the api already sends 10 activities only but slicing in case of realtime updates */}
); }