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 { useEffect as useEffectForToasts } from "react"; 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"; // Helper function to format last sync time function formatLastSyncTime(date: Date | null): string { if (!date) return "Never"; const now = new Date(); const syncDate = new Date(date); const diffMs = now.getTime() - syncDate.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); // Show relative time for recent syncs if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hr${diffHours === 1 ? '' : 's'} ago`; if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`; // For older syncs, show week count const diffWeeks = Math.floor(diffDays / 7); if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks === 1 ? '' : 's'} ago`; // For even older, show month count const diffMonths = Math.floor(diffDays / 30); return `${diffMonths} month${diffMonths === 1 ? '' : 's'} ago`; } // Helper function to format full timestamp function formatFullTimestamp(date: Date | null): string { if (!date) return ""; return new Date(date).toLocaleString("en-US", { month: "2-digit", day: "2-digit", year: "2-digit", hour: "2-digit", minute: "2-digit", hour12: true }).replace(',', ''); } 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]); }, []); // Use the SSE hook const { connected } = useSSE({ userId: user?.id, onMessage: handleNewMessage, }); // Setup rate limit event listener for toast notifications useEffectForToasts(() => { if (!user?.id) return; const eventSource = new EventSource(`/api/events?userId=${user.id}`); eventSource.addEventListener("rate-limit", (event) => { try { const data = JSON.parse(event.data); switch (data.type) { case "warning": // 80% threshold warning toast.warning("GitHub API Rate Limit Warning", { description: data.message, duration: 8000, }); break; case "exceeded": // 100% rate limit exceeded toast.error("GitHub API Rate Limit Exceeded", { description: data.message, duration: 10000, }); break; case "resumed": // Rate limit reset notification toast.success("Rate Limit Reset", { description: "API operations have resumed.", duration: 5000, }); break; } } catch (error) { console.error("Error parsing rate limit event:", error); } }); return () => { eventSource.close(); }; }, [user?.id]); // 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="Total imported repositories" /> } description="Synced to Gitea" /> } description="From GitHub" /> } description={formatFullTimestamp(lastSync)} />
); }