import { useEffect, useRef, useState } from "react"; import { SidebarProvider, useSidebar } from "../context/SidebarContext"; import { Outlet } from "react-router-dom"; import AppHeader from "./AppHeader"; import Backdrop from "./Backdrop"; import AppSidebar from "./AppSidebar"; import { useSiteStore } from "../store/siteStore"; import { useAuthStore } from "../store/authStore"; import { useBillingStore } from "../store/billingStore"; import { useHeaderMetrics } from "../context/HeaderMetricsContext"; import { useErrorHandler } from "../hooks/useErrorHandler"; import { trackLoading } from "../components/common/LoadingStateMonitor"; import PendingPaymentBanner from "../components/billing/PendingPaymentBanner"; const LayoutContent: React.FC = () => { const { isExpanded, isHovered, isMobileOpen } = useSidebar(); const { loadActiveSite, activeSite } = useSiteStore(); const { refreshUser, isAuthenticated } = useAuthStore(); const { balance, loadBalance } = useBillingStore(); const { setMetrics } = useHeaderMetrics(); const { addError } = useErrorHandler('AppLayout'); const hasLoadedSite = useRef(false); const isLoadingSite = useRef(false); const lastUserRefresh = useRef(0); // Initialize site store on mount - only once, but only if authenticated useEffect(() => { // Only load sites if user is authenticated AND has a token if (!isAuthenticated) return; // Check if token exists - if not, wait a bit for Zustand persist to write it const checkTokenAndLoad = () => { const authState = useAuthStore.getState(); if (!authState?.token) { // Token not available yet - wait a bit and retry (Zustand persist might still be writing) setTimeout(() => { const retryAuthState = useAuthStore.getState(); if (retryAuthState?.token && !hasLoadedSite.current && !isLoadingSite.current) { loadSites(); } }, 100); // Wait 100ms for persist to write return; } loadSites(); }; const loadSites = () => { if (!hasLoadedSite.current && !isLoadingSite.current) { hasLoadedSite.current = true; isLoadingSite.current = true; trackLoading('site-loading', true); // Add timeout to prevent infinite loading // Match API timeout (30s) + buffer for network delays const timeoutId = setTimeout(() => { if (isLoadingSite.current) { console.error('AppLayout: Site loading timeout after 35 seconds'); trackLoading('site-loading', false); isLoadingSite.current = false; addError(new Error('Site loading timeout - check network connection'), 'AppLayout.loadActiveSite'); } }, 35000); // 35 seconds to match API timeout (30s) + buffer loadActiveSite() .catch((error) => { // Don't log 403 errors as they're expected when not authenticated if (error.status !== 403) { console.error('AppLayout: Error loading active site:', error); addError(error, 'AppLayout.loadActiveSite'); } }) .finally(() => { clearTimeout(timeoutId); trackLoading('site-loading', false); isLoadingSite.current = false; }); } }; checkTokenAndLoad(); }, [isAuthenticated]); // Run when authentication state changes // All session refresh logic removed - API interceptor handles token refresh automatically on 401 // Load credit balance and set in header metrics useEffect(() => { if (!isAuthenticated) { setMetrics([]); return; } const billingState = useBillingStore.getState(); // Load balance if not already loaded and not currently loading if (!balance && !billingState.loading) { loadBalance().catch((error) => { console.error('AppLayout: Error loading credit balance:', error); // Don't show error to user - balance is not critical for app functionality // But retry after a delay setTimeout(() => { if (!useBillingStore.getState().balance && !useBillingStore.getState().loading) { loadBalance().catch(() => { // Silently fail on retry too }); } }, 5000); }); } }, [isAuthenticated, balance, loadBalance, setMetrics]); // Update header metrics when balance changes // This sets credit balance which will be merged with page metrics by HeaderMetricsContext useEffect(() => { if (!isAuthenticated) { // Only clear metrics when not authenticated (user logged out) setMetrics([]); return; } // If balance is null, don't clear metrics - let page metrics stay visible // Only set credit metrics when balance is loaded if (!balance) { return; } // Determine accent color based on content pieces remaining const remaining = balance.credits_remaining ?? balance.credits; const total = balance.plan_credits_per_month ?? 0; const usagePercent = total > 0 ? (remaining / total) * 100 : 100; let accentColor: 'blue' | 'green' | 'amber' | 'purple' = 'blue'; if (usagePercent > 50) { accentColor = 'green'; } else if (usagePercent > 20) { accentColor = 'blue'; } else if (usagePercent > 0) { accentColor = 'amber'; } else { accentColor = 'purple'; } // Format credit value with K/M suffix for large numbers const formatCredits = (val: number): string => { if (val >= 1000000) { const millions = val / 1000000; return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`; } if (val >= 1000) { const thousands = val / 1000; return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`; } return val.toString(); }; // Set credit balance - show as "used/total Credits" setMetrics([{ label: 'Credits', value: total > 0 ? `${formatCredits(remaining)}/${formatCredits(total)}` : formatCredits(remaining), accentColor, }]); }, [balance, isAuthenticated, setMetrics]); return (
{/* Pending Payment Banner - Shows when account status is 'pending_payment' */}
); }; const AppLayout: React.FC = () => { return ( ); }; export default AppLayout;