modcomms/frontend/App.tsx
michael abfcb6aae2 Fix campaign status change not persisting in UI
Use optimistic update pattern to immediately reflect status changes
in the UI, with rollback on API error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 09:31:59 -06:00

787 lines
30 KiB
TypeScript
Executable file

import React, { useState, useEffect } from 'react';
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { Hero } from './components/Hero';
import { analyzeProof } from './services/geminiService';
import { getUserInfo } from './services/authService';
import apiService from './services/apiService';
import type { AgentReview, AgentName, FlaggedItem, ResolvedItem, ErrorItem } from './types';
import { AGENT_NAMES } from './constants';
import { Sidebar } from './components/Sidebar';
import { ChecksOverview } from './components/ChecksOverview';
import { Analytics } from './components/Analytics';
import { Profile } from './components/Profile';
import { CopyGenAI } from './components/CopyGenAI';
import { Settings } from './components/Settings';
import { Campaigns } from './components/Campaigns';
import { Auditing } from './components/Auditing';
import { Login } from './components/Login';
import { WIPReviewer } from './components/WIPReviewer';
type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing';
export interface DropdownOptions {
campaigns: string[];
// Hierarchy: Channel -> SubChannel -> ProofType[]
channels: Record<string, Record<string, string[]>>;
// Brand guidelines for campaign creation
brandGuidelines: string[];
}
const App: React.FC = () => {
// MSAL authentication state
const isAuthenticated = useIsAuthenticated();
const { instance: msalInstance, inProgress } = useMsal();
const [currentView, setCurrentView] = useState<View>('Home');
const [selectedCampaign, setSelectedCampaign] = useState<string | null>(null);
const [selectedProof, setSelectedProof] = useState<any | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoadingData, setIsLoadingData] = useState(true);
// Initialize API service with MSAL instance for authenticated requests
useEffect(() => {
if (msalInstance) {
apiService.setMsalInstance(msalInstance);
}
}, [msalInstance]);
// Dropdown options now loaded from API
const [dropdownOptions, setDropdownOptions] = useState<DropdownOptions>({
campaigns: [],
channels: {},
brandGuidelines: []
});
// Load dropdown options from API when authenticated
useEffect(() => {
const loadDropdownOptions = async () => {
if (!isAuthenticated) return;
try {
const options = await apiService.getDropdownOptions();
setDropdownOptions({
campaigns: options.campaigns || [],
channels: options.channels || {},
brandGuidelines: options.brand_guidelines || []
});
} catch (error) {
console.error('Failed to load dropdown options:', error);
// Fall back to default options if API fails
setDropdownOptions({
campaigns: [],
channels: {
"Social": { "Meta": ["In-feed 1x1", "In-feed 4x5"] },
"Display": { "Banner": ["300x600", "300x250"] }
},
brandGuidelines: ["Barclays", "Barclaycard"]
});
}
};
loadDropdownOptions();
}, [isAuthenticated]);
// Campaigns and proofs now loaded from API instead of localStorage
const [campaigns, setCampaigns] = useState<any[]>([]);
const [campaignProofs, setCampaignProofs] = useState<Record<string, any[]>>({});
// Load campaigns from API when authenticated
useEffect(() => {
const loadCampaigns = async () => {
if (!isAuthenticated) return;
setIsLoadingData(true);
try {
const response = await apiService.getCampaigns();
setCampaigns(response.map(c => apiService.convertCampaignToFrontend(c)));
} catch (error) {
console.error('Failed to load campaigns:', error);
setError('Failed to load campaigns. Please try again.');
} finally {
setIsLoadingData(false);
}
};
loadCampaigns();
}, [isAuthenticated]);
// Audit items now loaded from API instead of localStorage
const [flaggedItems, setFlaggedItems] = useState<FlaggedItem[]>([]);
const [resolvedItems, setResolvedItems] = useState<ResolvedItem[]>([]);
const [errorItems, setErrorItems] = useState<ErrorItem[]>([]);
// Load audit items from API when authenticated
useEffect(() => {
const loadAuditItems = async () => {
if (!isAuthenticated) return;
try {
const [flagged, resolved, errors] = await Promise.all([
apiService.getFlaggedItems(),
apiService.getResolvedItems(),
apiService.getErrorItems(),
]);
setFlaggedItems(flagged.map(i => apiService.convertFlaggedItemToFrontend(i)));
setResolvedItems(resolved.map(i => apiService.convertResolvedItemToFrontend(i)));
setErrorItems(errors.map(i => apiService.convertErrorItemToFrontend(i)));
} catch (error) {
console.error('Failed to load audit items:', error);
}
};
loadAuditItems();
}, [isAuthenticated]);
useEffect(() => {
// Keep selectedProof in sync with the master list in campaignProofs.
// This ensures that when a new version is added, the detail view refreshes.
if (selectedCampaign && selectedProof && campaignProofs[selectedCampaign]) {
const currentCampaignProofs = campaignProofs[selectedCampaign];
const freshProof = currentCampaignProofs.find(a => !a.tempId && a.proofName === selectedProof.proofName);
// A simple stringify check to prevent re-renders if the object is the same.
if (freshProof && JSON.stringify(freshProof) !== JSON.stringify(selectedProof)) {
setSelectedProof(freshProof);
}
}
}, [campaignProofs, selectedCampaign, selectedProof]);
const handleAddNewCampaign = async (campaignData: { name: string; workfrontId: string; clientLead: string; agencyLead: string; brandGuidelines: string; }) => {
try {
const response = await apiService.createCampaign({
name: campaignData.name,
workfront_id: campaignData.workfrontId,
client_lead: campaignData.clientLead,
agency_lead: campaignData.agencyLead,
brand_guidelines: campaignData.brandGuidelines,
});
const newCampaign = apiService.convertCampaignToFrontend(response);
setCampaigns(prev => [...prev, newCampaign]);
setCampaignProofs(prev => ({ ...prev, [newCampaign.name]: [] }));
} catch (error) {
console.error('Error creating campaign:', error);
setError('Failed to create campaign. Please try again.');
}
};
const fileToDataUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
const handleProofUploadForCampaign = async (
campaignName: string,
file: File,
proofName: string,
channel: string,
subChannel: string,
proofType?: string
) => {
setError(null);
// Find the campaign to get its ID for API persistence
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found. Please refresh and try again.');
return;
}
const tempId = `temp_${Date.now()}`;
const newProofPlaceholder = {
tempId,
proofName,
channel,
subChannel,
proofType,
status: 'analyzing',
analysisProgress: { completed: 0, total: AGENT_NAMES.length + 1 },
file,
versions: [],
};
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: [newProofPlaceholder, ...(prevProofs[campaignName] || [])]
}));
const handleAgentUpdate = (agentName: AgentName | 'Summary') => {
setCampaignProofs(prevProofs => {
const currentProofs = prevProofs[campaignName] || [];
const updatedProofs = currentProofs.map(proof => {
if (proof.tempId === tempId && proof.status === 'analyzing') {
const newCompleted = (proof.analysisProgress?.completed ?? 0) + 1;
return { ...proof, analysisProgress: { ...proof.analysisProgress, completed: newCompleted } };
}
return proof;
});
return { ...prevProofs, [campaignName]: updatedProofs };
});
};
try {
// Pass campaign context to persist proof in database
const result = await analyzeProof(file, handleAgentUpdate, msalInstance, {
campaignId: campaign._id,
proofName,
channel,
subChannel,
proofType,
});
const feedback = result.review;
// Refresh proofs from API to get the persisted data
// This ensures we have the correct IDs and data from the database
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[campaignName]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (refreshError) {
console.error('Failed to refresh proofs after analysis:', refreshError);
// Fallback: remove the temp placeholder since analysis succeeded
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.tempId !== tempId)
}));
}
// Refresh campaigns list to get updated proof count
try {
const campaignsResponse = await apiService.getCampaigns();
setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c)));
} catch (refreshError) {
console.error('Failed to refresh campaigns after analysis:', refreshError);
}
// If analysis resulted in error, log it (error items are now tracked on backend)
if (feedback.overallStatus === 'Analysis Error') {
console.warn('Proof analysis resulted in error status:', feedback.leadAgentSummary);
}
} catch (err) {
console.error("Failed to upload and analyze proof:", err);
setError("Failed to upload and analyze proof. Please try again.");
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].map(proof =>
proof.tempId === tempId ? { ...proof, status: 'error' } : proof
)
}));
}
};
const handleRetryAnalysis = async (campaignName: string, tempId: string) => {
const proofToRetry = campaignProofs[campaignName]?.find(proof => proof.tempId === tempId);
if (!proofToRetry || !proofToRetry.file) {
console.error("Proof to retry not found or file is missing");
return;
}
// Find the campaign to get its ID for API persistence
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found. Please refresh and try again.');
return;
}
const { file, proofName, channel, subChannel, proofType } = proofToRetry;
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].map(proof =>
proof.tempId === tempId ? { ...proof, status: 'analyzing', analysisProgress: { completed: 0, total: AGENT_NAMES.length + 1 } } : proof
)
}));
const handleAgentUpdateForRetry = (agentName: AgentName | 'Summary') => {
setCampaignProofs(prevProofs => {
const currentProofs = prevProofs[campaignName] || [];
const updatedProofs = currentProofs.map(proof => {
if (proof.tempId === tempId && proof.status === 'analyzing') {
const newCompleted = (proof.analysisProgress?.completed ?? 0) + 1;
return { ...proof, analysisProgress: { ...proof.analysisProgress, completed: newCompleted } };
}
return proof;
});
return { ...prevProofs, [campaignName]: updatedProofs };
});
};
try {
// Pass campaign context to persist proof in database
const result = await analyzeProof(file, handleAgentUpdateForRetry, msalInstance, {
campaignId: campaign._id,
proofName,
channel,
subChannel,
proofType,
});
// Refresh proofs from API to get the persisted data
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[campaignName]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (refreshError) {
console.error('Failed to refresh proofs after retry:', refreshError);
// Remove the temp placeholder since analysis succeeded
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.tempId !== tempId)
}));
}
// Refresh campaigns list to get updated proof count
try {
const campaignsResponse = await apiService.getCampaigns();
setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c)));
} catch (refreshError) {
console.error('Failed to refresh campaigns after retry:', refreshError);
}
if (result.review.overallStatus === 'Analysis Error') {
console.warn('Retry analysis resulted in error status:', result.review.leadAgentSummary);
}
} catch (err) {
console.error("Failed to retry proof analysis:", err);
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].map(proof =>
proof.tempId === tempId ? { ...proof, status: 'error' } : proof
)
}));
}
};
const handleCampaignStatusChange = async (campaignName: string, newStatus: 'In Progress' | 'Completed') => {
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found.');
return;
}
const previousStatus = campaign.status;
const previousLastModified = campaign.lastModified;
// Optimistically update local state immediately
setCampaigns(prev => prev.map(p =>
p.name === campaignName ? { ...p, status: newStatus, lastModified: new Date().toISOString() } : p
));
try {
await apiService.updateCampaign(campaign._id, { status: newStatus });
} catch (error) {
console.error('Error updating campaign status:', error);
setError('Failed to update campaign status.');
// Rollback on error
setCampaigns(prev => prev.map(p =>
p.name === campaignName ? { ...p, status: previousStatus, lastModified: previousLastModified } : p
));
}
};
const handleDeleteProof = async (campaignName: string, proofName: string) => {
// Find the proof to get its ID
const proofToDelete = campaignProofs[campaignName]?.find(p => p.proofName === proofName);
if (!proofToDelete?._id) {
// If no _id, it might be a temp proof that hasn't been saved to DB yet
// Just remove it from local state
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.proofName !== proofName)
}));
return;
}
try {
await apiService.deleteProof(proofToDelete._id);
// Update local state after successful deletion
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.proofName !== proofName)
}));
// Refresh campaigns to get updated proof count
const campaignsResponse = await apiService.getCampaigns();
setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c)));
} catch (error) {
console.error('Error deleting proof:', error);
setError('Failed to delete proof.');
}
};
// --- SETTINGS HANDLERS (NOW USE API) ---
// Helper to refresh dropdown options from API
const refreshDropdownOptions = async () => {
try {
const options = await apiService.getDropdownOptions();
setDropdownOptions({
campaigns: options.campaigns || [],
channels: options.channels || {},
brandGuidelines: options.brand_guidelines || []
});
} catch (error) {
console.error('Failed to refresh dropdown options:', error);
}
};
// Campaign options are not stored in dropdown_options table - keeping local for now
const handleAddCampaignOption = (value: string) => {
setDropdownOptions(prev => {
if (!prev.campaigns.includes(value)) {
return { ...prev, campaigns: [...prev.campaigns, value].sort() };
}
return prev;
});
};
const handleRemoveCampaignOption = (value: string) => {
setDropdownOptions(prev => ({ ...prev, campaigns: prev.campaigns.filter(c => c !== value) }));
};
const handleAddChannel = async (channel: string) => {
try {
await apiService.addChannel(channel);
// Optimistically update local state
setDropdownOptions(prev => ({
...prev,
channels: { ...prev.channels, [channel]: {} }
}));
} catch (error) {
console.error('Error adding channel:', error);
setError('Failed to add channel.');
}
};
const handleRemoveChannel = async (channel: string) => {
try {
await apiService.deleteChannel(channel);
// Optimistically update local state
setDropdownOptions(prev => {
const newChannels = { ...prev.channels };
delete newChannels[channel];
return { ...prev, channels: newChannels };
});
} catch (error) {
console.error('Error removing channel:', error);
setError('Failed to remove channel.');
}
};
const handleAddSubChannel = async (channel: string, subChannel: string) => {
try {
await apiService.addSubChannel(channel, subChannel);
// Optimistically update local state
setDropdownOptions(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: { ...prev.channels[channel], [subChannel]: [] }
}
}));
} catch (error) {
console.error('Error adding sub-channel:', error);
setError('Failed to add sub-channel.');
}
};
const handleRemoveSubChannel = async (channel: string, subChannel: string) => {
try {
await apiService.deleteSubChannel(channel, subChannel);
// Optimistically update local state
setDropdownOptions(prev => {
const channelData = { ...prev.channels[channel] };
delete channelData[subChannel];
return { ...prev, channels: { ...prev.channels, [channel]: channelData } };
});
} catch (error) {
console.error('Error removing sub-channel:', error);
setError('Failed to remove sub-channel.');
}
};
const handleAddProofType = async (channel: string, subChannel: string, proofType: string) => {
try {
await apiService.addProofType(channel, subChannel, proofType);
// Optimistically update local state
setDropdownOptions(prev => {
const channelData = prev.channels[channel] || {};
const currentTypes = channelData[subChannel] || [];
return {
...prev,
channels: {
...prev.channels,
[channel]: {
...channelData,
[subChannel]: [...currentTypes, proofType].sort()
}
}
};
});
} catch (error) {
console.error('Error adding proof type:', error);
setError('Failed to add proof type.');
}
};
const handleRemoveProofType = async (channel: string, subChannel: string, proofType: string) => {
try {
await apiService.deleteProofType(channel, subChannel, proofType);
// Optimistically update local state
setDropdownOptions(prev => {
const channelData = prev.channels[channel];
if (!channelData) return prev;
const currentTypes = channelData[subChannel] || [];
return {
...prev,
channels: {
...prev.channels,
[channel]: {
...channelData,
[subChannel]: currentTypes.filter(t => t !== proofType)
}
}
};
});
} catch (error) {
console.error('Error removing proof type:', error);
setError('Failed to remove proof type.');
}
};
const handleNavigate = (view: View) => {
setCurrentView(view);
setSelectedCampaign(null); // Reset campaign on any main navigation
setSelectedProof(null);
};
const handleSelectCampaign = async (campaignName: string) => {
setSelectedCampaign(campaignName);
// Find the campaign to get its ID for API calls
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
console.error('Campaign not found or missing ID:', campaignName);
return;
}
// Load proofs from API if not already cached
if (!campaignProofs[campaignName]) {
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[campaignName]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (error) {
console.error('Failed to load proofs for campaign:', campaignName, error);
setError('Failed to load proofs. Please try again.');
}
}
};
const handleSelectProof = (proof: any) => {
setSelectedProof(proof);
};
const handleBackToCampaignsList = () => {
setSelectedCampaign(null);
setSelectedProof(null);
};
const handleBackToCampaignDetails = () => {
setSelectedProof(null);
};
const handleFlagSubmit = async (flagData: Omit<FlaggedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
// Find the proof to get its ID
const proof = campaignProofs[flagData.campaignName]?.find(p => p.proofName === flagData.proofName);
if (!proof?._id) {
setError('Proof not found. Unable to submit flag.');
return;
}
try {
const response = await apiService.flagProofVersion(
proof._id,
flagData.version,
{ agent_flagged: flagData.agentFlagged, comments: flagData.comments }
);
const newFlag = apiService.convertFlaggedItemToFrontend(response);
setFlaggedItems(prev => [newFlag, ...prev]);
} catch (error) {
console.error('Error flagging proof version:', error);
setError('Failed to submit flag. Please try again.');
}
};
const handleResolveSubmit = async (resolveData: Omit<ResolvedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
// Find the proof to get its ID
const proof = campaignProofs[resolveData.campaignName]?.find(p => p.proofName === resolveData.proofName);
if (!proof?._id) {
setError('Proof not found. Unable to submit resolution.');
return;
}
try {
const response = await apiService.resolveProofVersion(
proof._id,
resolveData.version,
{ agent: resolveData.agent, issue: resolveData.issue, resolution: resolveData.resolution }
);
const newResolution = apiService.convertResolvedItemToFrontend(response);
setResolvedItems(prev => [newResolution, ...prev]);
} catch (error) {
console.error('Error resolving proof version:', error);
setError('Failed to submit resolution. Please try again.');
}
};
const handleNavigateToAuditedItem = (item: { campaignName: string; proofName: string; version: number }) => {
const proofToSelect = campaignProofs[item.campaignName]?.find(a => a.proofName === item.proofName);
if (proofToSelect) {
const versionExists = proofToSelect.versions.some((v: any) => v.version === item.version);
if (versionExists) {
setSelectedCampaign(item.campaignName);
// Add a temporary property to the proof object to indicate which version to show.
setSelectedProof({ ...proofToSelect, initialVersion: item.version });
setCurrentView('Campaigns');
} else {
setError(`Version ${item.version} not found for proof ${item.proofName}. It may have been deleted.`);
}
} else {
setError(`Proof ${item.proofName} not found in campaign ${item.campaignName}. It may have been deleted.`);
}
};
const handleLogout = async () => {
try {
await msalInstance.logoutPopup({
postLogoutRedirectUri: window.location.origin,
});
} catch (error) {
console.error('Logout failed:', error);
}
};
const renderContent = () => {
switch (currentView) {
case 'Analytics':
return <Analytics />;
case 'Profile':
return <Profile onLogout={handleLogout} msalInstance={msalInstance} />;
case 'CopyGenAI':
return <CopyGenAI />;
case 'Campaigns':
return <Campaigns
selectedCampaign={selectedCampaign}
selectedProof={selectedProof}
onSelectCampaign={handleSelectCampaign}
onSelectProof={handleSelectProof}
onBackToCampaignsList={handleBackToCampaignsList}
onBackToCampaignDetails={handleBackToCampaignDetails}
campaigns={campaigns}
campaignProofs={campaignProofs}
onAddNewCampaign={handleAddNewCampaign}
onProofUpload={handleProofUploadForCampaign}
dropdownOptions={dropdownOptions}
onRetryAnalysis={handleRetryAnalysis}
onCampaignStatusChange={handleCampaignStatusChange}
onDeleteProof={handleDeleteProof}
onFlagSubmit={handleFlagSubmit}
onResolveSubmit={handleResolveSubmit}
/>;
case 'WIP Reviewer':
return <WIPReviewer dropdownOptions={dropdownOptions} msalInstance={msalInstance} />;
case 'Auditing':
return <Auditing
flaggedItems={flaggedItems}
resolvedItems={resolvedItems}
errorItems={errorItems}
onNavigate={handleNavigateToAuditedItem}
/>;
case 'Settings':
return <Settings
options={dropdownOptions}
onAddCampaign={handleAddCampaignOption}
onRemoveCampaign={handleRemoveCampaignOption}
onAddChannel={handleAddChannel}
onRemoveChannel={handleRemoveChannel}
onAddSubChannel={handleAddSubChannel}
onRemoveSubChannel={handleRemoveSubChannel}
onAddProofType={handleAddProofType}
onRemoveProofType={handleRemoveProofType}
onNavigate={handleNavigate}
/>;
case 'Home':
default:
return (
<>
<Hero onGetStarted={() => handleNavigate('Campaigns')} />
<ChecksOverview />
</>
);
}
};
// Show loading spinner during MSAL authentication interactions
if (inProgress !== InteractionStatus.None) {
return (
<div className="fixed inset-0 bg-[#0f172a] flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<svg className="animate-spin h-12 w-12 text-brand-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-white/80 text-sm">Authenticating...</p>
</div>
</div>
);
}
if (!isAuthenticated) {
return <Login />;
}
// Determine background color based on view to avoid grey bar on Home view
const mainBgColor = currentView === 'Home' ? 'bg-white' : 'bg-brand-gray';
// Get user info from MSAL for sidebar display
const userInfo = getUserInfo(msalInstance);
return (
<div className={`flex h-screen ${mainBgColor} font-sans text-gray-800 overflow-hidden`}>
<Sidebar
activeItem={currentView}
onNavigate={(view) => handleNavigate(view as View)}
userName={userInfo?.name}
userEmail={userInfo?.email}
/>
<div className="flex-1 flex flex-col overflow-y-auto">
<main className="flex-1 flex flex-col min-h-full">
{renderContent()}
</main>
</div>
</div>
);
};
export default App;