Added onboarding progress tracking & landing page

This commit is contained in:
ajaysi
2025-10-02 13:20:15 +05:30
parent e57d2577f8
commit 510b79bbf8
135 changed files with 25917 additions and 5768 deletions

View File

@@ -0,0 +1,145 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Box, Typography, Button, Alert, Stack } from '@mui/material';
import { Refresh as RefreshIcon } from '@mui/icons-material';
interface ComponentErrorBoundaryProps {
children: ReactNode;
componentName: string;
onReset?: () => void;
}
interface ComponentErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
/**
* Lightweight Error Boundary for Individual Components
*
* Use this to wrap specific components that might fail without crashing the entire app.
* Shows a minimal error UI that doesn't take over the whole page.
*
* Usage:
* <ComponentErrorBoundary componentName="API Key Carousel">
* <ApiKeyCarousel />
* </ComponentErrorBoundary>
*/
class ComponentErrorBoundary extends Component<
ComponentErrorBoundaryProps,
ComponentErrorBoundaryState
> {
constructor(props: ComponentErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error: Error): Partial<ComponentErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error(`Error in ${this.props.componentName}:`, error, errorInfo);
// Log to backend or error tracking service
this.logError(error, errorInfo);
}
logError(error: Error, errorInfo: ErrorInfo) {
try {
// Import error reporting utility
import('../../utils/errorReporting').then(({ reportError }) => {
reportError({
error,
context: `Component: ${this.props.componentName}`,
metadata: {
componentStack: errorInfo.componentStack,
componentError: true,
},
severity: 'medium', // Component errors are medium severity
timestamp: new Date().toISOString(),
});
}).catch(console.error);
console.group(`🔴 Component Error: ${this.props.componentName}`);
console.error('Error:', error.message);
console.error('Stack:', error.stack);
console.error('Component Stack:', errorInfo.componentStack);
console.groupEnd();
} catch (e) {
console.error('Failed to log component error:', e);
}
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
});
if (this.props.onReset) {
this.props.onReset();
}
};
render() {
if (this.state.hasError) {
return (
<Alert
severity="error"
sx={{
my: 2,
borderRadius: 2,
}}
action={
<Button
color="inherit"
size="small"
onClick={this.handleReset}
startIcon={<RefreshIcon />}
>
Retry
</Button>
}
>
<Stack spacing={1}>
<Typography variant="subtitle2" fontWeight={600}>
{this.props.componentName} Error
</Typography>
<Typography variant="body2">
{this.state.error?.message || 'An unexpected error occurred in this component.'}
</Typography>
{process.env.NODE_ENV === 'development' && this.state.error?.stack && (
<Typography
variant="caption"
component="pre"
sx={{
mt: 1,
p: 1,
bgcolor: 'rgba(0,0,0,0.05)',
borderRadius: 1,
fontSize: '0.7rem',
maxHeight: 100,
overflowY: 'auto',
overflowX: 'auto',
}}
>
{this.state.error.stack}
</Typography>
)}
</Stack>
</Alert>
);
}
return this.props.children;
}
}
export default ComponentErrorBoundary;

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Box, Typography, Chip, Button, CircularProgress, Tooltip } from '@mui/material';
import { PlayArrow, Pause, Stop } from '@mui/icons-material';
import { ShimmerHeader } from './styled';
import UserBadge from './UserBadge';
import { DashboardHeaderProps } from './types';
const DashboardHeader: React.FC<DashboardHeaderProps> = ({
@@ -402,6 +403,7 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
</Box>
)}
{rightContent}
<UserBadge colorMode="dark" />
</Box>
</Box>
</ShimmerHeader>

View File

@@ -0,0 +1,392 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import {
Box,
Button,
Typography,
Paper,
Container,
Stack,
Alert,
Collapse,
IconButton,
Divider
} from '@mui/material';
import {
ErrorOutline as ErrorIcon,
Refresh as RefreshIcon,
Home as HomeIcon,
ExpandMore as ExpandMoreIcon,
BugReport as BugReportIcon
} from '@mui/icons-material';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
showDetails?: boolean;
context?: string; // Context for better error messages (e.g., "Onboarding Wizard")
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
showDetails: boolean;
}
/**
* ErrorBoundary Component
*
* Catches JavaScript errors anywhere in the child component tree,
* logs those errors, and displays a fallback UI instead of blank screen.
*
* Usage:
* <ErrorBoundary context="Dashboard">
* <YourComponent />
* </ErrorBoundary>
*/
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
// Update state so the next render will show the fallback UI
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log error details
console.error('ErrorBoundary caught an error:', error, errorInfo);
// Update state with error info
this.setState({
error,
errorInfo,
});
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// Send to error tracking service (Sentry, LogRocket, etc.)
this.logErrorToService(error, errorInfo);
}
logErrorToService(error: Error, errorInfo: ErrorInfo) {
try {
// Import error reporting utility
import('../../utils/errorReporting').then(({ reportError }) => {
reportError({
error,
context: this.props.context || 'ErrorBoundary',
metadata: {
componentStack: errorInfo.componentStack,
errorBoundary: true,
},
severity: 'high', // Rendering errors are high severity
timestamp: new Date().toISOString(),
});
}).catch(console.error);
// Log to console with detailed info
console.group('🚨 Error Boundary - Error Details');
console.error('Error:', error);
console.error('Error Info:', errorInfo);
console.error('Component Stack:', errorInfo.componentStack);
console.error('Context:', this.props.context || 'Global');
console.error('Timestamp:', new Date().toISOString());
console.groupEnd();
} catch (loggingError) {
console.error('Failed to log error:', loggingError);
}
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
});
};
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
toggleDetails = () => {
this.setState((prevState) => ({
showDetails: !prevState.showDetails,
}));
};
render() {
if (this.state.hasError) {
// Custom fallback UI provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback UI
const { error, errorInfo, showDetails } = this.state;
const { context, showDetails: showDetailsDefault } = this.props;
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
p: { xs: 2, md: 4 },
}}
>
<Container maxWidth="md">
<Paper
elevation={24}
sx={{
p: { xs: 3, md: 5 },
borderRadius: 4,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
}}
>
<Stack spacing={3} alignItems="center">
{/* Error Icon */}
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
background: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
}}
>
<ErrorIcon sx={{ fontSize: 48, color: 'white' }} />
</Box>
{/* Error Title */}
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: 700,
color: '#1a1a1a',
textAlign: 'center',
}}
>
Oops! Something went wrong
</Typography>
{/* Context Message */}
{context && (
<Typography
variant="body1"
color="text.secondary"
sx={{ textAlign: 'center' }}
>
An error occurred in: <strong>{context}</strong>
</Typography>
)}
{/* User-friendly message */}
<Typography
variant="body1"
color="text.secondary"
sx={{ textAlign: 'center', maxWidth: 600 }}
>
We're sorry for the inconvenience. The error has been logged and our team will investigate.
In the meantime, you can try refreshing the page or returning to the home page.
</Typography>
{/* Action Buttons */}
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ mt: 2, width: '100%', maxWidth: 500 }}
>
<Button
variant="contained"
size="large"
startIcon={<RefreshIcon />}
onClick={this.handleReload}
sx={{
flex: 1,
py: 1.5,
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
'&:hover': {
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
},
}}
>
Reload Page
</Button>
<Button
variant="outlined"
size="large"
startIcon={<HomeIcon />}
onClick={this.handleGoHome}
sx={{
flex: 1,
py: 1.5,
borderColor: '#667eea',
color: '#667eea',
'&:hover': {
borderColor: '#5568d3',
background: 'rgba(102, 126, 234, 0.05)',
},
}}
>
Go Home
</Button>
</Stack>
{/* Error Details Toggle (for developers/debugging) */}
{(showDetailsDefault || process.env.NODE_ENV === 'development') && (
<>
<Divider sx={{ width: '100%', my: 2 }} />
<Button
variant="text"
size="small"
startIcon={<BugReportIcon />}
endIcon={
<ExpandMoreIcon
sx={{
transform: showDetails ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.3s',
}}
/>
}
onClick={this.toggleDetails}
sx={{ color: '#666' }}
>
{showDetails ? 'Hide' : 'Show'} Technical Details
</Button>
<Collapse in={showDetails} sx={{ width: '100%' }}>
<Alert
severity="error"
icon={<BugReportIcon />}
sx={{
textAlign: 'left',
'& .MuiAlert-message': {
width: '100%',
},
}}
>
<Typography variant="subtitle2" fontWeight={600} gutterBottom>
Error Message:
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: 'monospace',
fontSize: '0.85rem',
mb: 2,
p: 1,
bgcolor: 'rgba(0,0,0,0.05)',
borderRadius: 1,
overflowX: 'auto',
}}
>
{error?.toString()}
</Typography>
{error?.stack && (
<>
<Typography variant="subtitle2" fontWeight={600} gutterBottom>
Stack Trace:
</Typography>
<Typography
variant="body2"
component="pre"
sx={{
fontFamily: 'monospace',
fontSize: '0.75rem',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
p: 1,
bgcolor: 'rgba(0,0,0,0.05)',
borderRadius: 1,
maxHeight: 200,
overflowY: 'auto',
}}
>
{error.stack}
</Typography>
</>
)}
{errorInfo?.componentStack && (
<>
<Typography variant="subtitle2" fontWeight={600} gutterBottom sx={{ mt: 2 }}>
Component Stack:
</Typography>
<Typography
variant="body2"
component="pre"
sx={{
fontFamily: 'monospace',
fontSize: '0.75rem',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
p: 1,
bgcolor: 'rgba(0,0,0,0.05)',
borderRadius: 1,
maxHeight: 150,
overflowY: 'auto',
}}
>
{errorInfo.componentStack}
</Typography>
</>
)}
</Alert>
</Collapse>
</>
)}
{/* Help Text */}
<Typography
variant="caption"
color="text.secondary"
sx={{ textAlign: 'center', mt: 2 }}
>
Error ID: {Date.now().toString(36)} Timestamp: {new Date().toLocaleString()}
</Typography>
</Stack>
</Paper>
</Container>
</Box>
);
}
// No error, render children normally
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -0,0 +1,203 @@
import React, { useState } from 'react';
import { Box, Button, Typography, Stack, Alert, Paper } from '@mui/material';
import { BugReport as BugReportIcon } from '@mui/icons-material';
import ErrorBoundary from './ErrorBoundary';
import ComponentErrorBoundary from './ComponentErrorBoundary';
/**
* Error Boundary Test Component
*
* Use this component to test that error boundaries are working correctly.
* Access via: http://localhost:3000/error-test (add route in App.tsx)
*
* This should ONLY be used in development!
*/
// Component that intentionally crashes
const CrashingComponent: React.FC<{ shouldCrash: boolean }> = ({ shouldCrash }) => {
if (shouldCrash) {
throw new Error('Intentional error for testing ErrorBoundary');
}
return <Typography>Component is working normally</Typography>;
};
// Component that crashes after a delay
const DelayedCrashComponent: React.FC<{ shouldCrash: boolean }> = ({ shouldCrash }) => {
const [count, setCount] = useState(0);
if (count > 3 && shouldCrash) {
throw new Error('Delayed crash after 3 clicks');
}
return (
<Box>
<Typography>Click count: {count}</Typography>
<Button onClick={() => setCount(count + 1)} variant="outlined" size="small">
Increment (crashes after 3)
</Button>
</Box>
);
};
const ErrorBoundaryTest: React.FC = () => {
const [globalCrash, setGlobalCrash] = useState(false);
const [componentCrash, setComponentCrash] = useState(false);
const [delayedCrash, setDelayedCrash] = useState(false);
return (
<Box sx={{ p: 4, maxWidth: 1200, mx: 'auto' }}>
<Paper sx={{ p: 4, mb: 4, bgcolor: 'warning.light' }}>
<Stack spacing={2}>
<Stack direction="row" spacing={2} alignItems="center">
<BugReportIcon sx={{ fontSize: 40 }} />
<Typography variant="h4" fontWeight={700}>
Error Boundary Testing
</Typography>
</Stack>
<Alert severity="warning">
<strong>Development Only:</strong> This page is for testing error boundaries.
Remove this route before deploying to production!
</Alert>
</Stack>
</Paper>
<Stack spacing={4}>
{/* Test 1: Global Error Boundary */}
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight={600}>
Test 1: Global Error Boundary
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
This will crash the entire component tree. The global ErrorBoundary should catch it
and show a full-page error screen with reload options.
</Typography>
<ErrorBoundary context="Global Error Test">
<Box sx={{ p: 2, border: '1px dashed', borderColor: 'divider', borderRadius: 1, mb: 2 }}>
<CrashingComponent shouldCrash={globalCrash} />
</Box>
</ErrorBoundary>
<Button
variant="contained"
color="error"
onClick={() => setGlobalCrash(true)}
disabled={globalCrash}
>
Trigger Global Crash
</Button>
</Paper>
{/* Test 2: Component-Level Error Boundary */}
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight={600}>
Test 2: Component Error Boundary
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
This will crash only a specific component. The ComponentErrorBoundary should show
a minimal error message inline without affecting the rest of the page.
</Typography>
<ComponentErrorBoundary
componentName="Test Component"
onReset={() => setComponentCrash(false)}
>
<Box sx={{ p: 2, border: '1px dashed', borderColor: 'divider', borderRadius: 1, mb: 2 }}>
<CrashingComponent shouldCrash={componentCrash} />
</Box>
</ComponentErrorBoundary>
<Button
variant="contained"
color="warning"
onClick={() => setComponentCrash(true)}
disabled={componentCrash}
>
Trigger Component Crash
</Button>
{componentCrash && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
Notice: Only the component crashed, not the entire page!
</Typography>
)}
</Paper>
{/* Test 3: Delayed Crash */}
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight={600}>
Test 3: Delayed Error (Simulates User Interaction)
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
This component crashes after user interaction (3 clicks). Tests that error boundaries
work for runtime errors, not just initial render errors.
</Typography>
<ComponentErrorBoundary
componentName="Delayed Crash Component"
onReset={() => setDelayedCrash(false)}
>
<Box sx={{ p: 2, border: '1px dashed', borderColor: 'divider', borderRadius: 1, mb: 2 }}>
<DelayedCrashComponent shouldCrash={delayedCrash} />
</Box>
</ComponentErrorBoundary>
<Button
variant="contained"
color="info"
onClick={() => setDelayedCrash(true)}
disabled={delayedCrash}
>
Enable Delayed Crash
</Button>
</Paper>
{/* Test 4: API Error Simulation */}
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight={600}>
Test 4: Verify Error Boundary Doesn't Catch API Errors
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
Error boundaries only catch rendering errors, not async errors.
This is expected behavior - API errors should be handled with try/catch.
</Typography>
<Alert severity="info">
Error boundaries do NOT catch:
<ul>
<li>Event handlers (onClick, onChange, etc.)</li>
<li>Asynchronous code (setTimeout, fetch, promises)</li>
<li>Server-side rendering errors</li>
<li>Errors in the error boundary itself</li>
</ul>
These should be handled with try/catch blocks.
</Alert>
</Paper>
{/* Instructions */}
<Paper sx={{ p: 3, bgcolor: 'success.light' }}>
<Typography variant="h6" gutterBottom fontWeight={600}>
Testing Instructions
</Typography>
<Stack spacing={1}>
<Typography variant="body2">
1. <strong>Global Crash:</strong> Should show full-page error with "Reload Page" and "Go Home" buttons
</Typography>
<Typography variant="body2">
2. <strong>Component Crash:</strong> Should show inline error alert with "Retry" button
</Typography>
<Typography variant="body2">
3. <strong>Delayed Crash:</strong> Click increment 4 times to trigger error
</Typography>
<Typography variant="body2">
4. <strong>Check Console:</strong> All errors should be logged with detailed stack traces
</Typography>
</Stack>
</Paper>
</Stack>
</Box>
);
};
export default ErrorBoundaryTest;

View File

@@ -1,57 +1,29 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { Navigate } from 'react-router-dom';
import { Box, CircularProgress, Typography } from '@mui/material';
import { apiClient } from '../../api/client';
import { useAuth } from '@clerk/clerk-react';
import { Box, CircularProgress, Typography, Alert, Button } from '@mui/material';
import { Refresh as RefreshIcon } from '@mui/icons-material';
import { useOnboarding } from '../../contexts/OnboardingContext';
interface ProtectedRouteProps {
children: React.ReactNode;
}
interface OnboardingStatus {
is_completed: boolean;
current_step: number;
completion_percentage: number;
next_step?: number;
started_at: string;
completed_at?: string;
can_proceed_to_final: boolean;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const [loading, setLoading] = useState(true);
const [onboardingComplete, setOnboardingComplete] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const checkOnboardingStatus = async () => {
try {
console.log('ProtectedRoute: Checking onboarding status...');
const response = await apiClient.get('/api/onboarding/status');
const status: OnboardingStatus = response.data;
console.log('ProtectedRoute: Onboarding status:', status);
if (status.is_completed) {
console.log('ProtectedRoute: Onboarding is complete, allowing access');
setOnboardingComplete(true);
} else {
console.log('ProtectedRoute: Onboarding not complete, redirecting to onboarding');
setOnboardingComplete(false);
}
} catch (err) {
console.error('ProtectedRoute: Error checking onboarding status:', err);
setError('Failed to check onboarding status');
// On error, assume onboarding is not complete for security
setOnboardingComplete(false);
} finally {
setLoading(false);
}
};
checkOnboardingStatus();
}, []);
const { isSignedIn } = useAuth();
// Use onboarding context instead of making API calls
const {
loading,
error,
isOnboardingComplete,
refresh,
clearError
} = useOnboarding();
// Loading state - show spinner
if (loading) {
console.log('ProtectedRoute: Loading onboarding state from context...');
return (
<Box
display="flex"
@@ -69,7 +41,9 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
);
}
// Error state - show error with retry
if (error) {
console.error('ProtectedRoute: Error from context:', error);
return (
<Box
display="flex"
@@ -83,24 +57,46 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
<Typography variant="h5" color="error" gutterBottom>
Access Error
</Typography>
<Typography variant="body1" color="textSecondary" textAlign="center">
<Alert
severity="error"
sx={{ maxWidth: 500, mb: 2 }}
action={
<Button
color="inherit"
size="small"
onClick={() => {
clearError();
refresh();
}}
startIcon={<RefreshIcon />}
>
Retry
</Button>
}
>
{error}
</Typography>
</Alert>
<Typography variant="body2" color="textSecondary" textAlign="center">
Please complete the setup process first.
Please try refreshing or complete the setup process first.
</Typography>
</Box>
);
}
// If onboarding is not complete, redirect to onboarding
if (!onboardingComplete) {
console.log('ProtectedRoute: Redirecting to onboarding');
// Not signed in - redirect to landing
if (!isSignedIn) {
console.log('ProtectedRoute: Not signed in, redirecting to landing');
return <Navigate to="/" replace />;
}
// Onboarding not complete - redirect to onboarding
if (!isOnboardingComplete) {
console.log('ProtectedRoute: Onboarding not complete (from context), redirecting');
return <Navigate to="/onboarding" replace />;
}
// If onboarding is complete, render the protected component
console.log('ProtectedRoute: Rendering protected component');
// All checks passed - render protected component
console.log('ProtectedRoute: Access granted (from context), rendering component');
return <>{children}</>;
};

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { Avatar, Box, Button, Menu, MenuItem, Typography, Tooltip } from '@mui/material';
import { useUser, useClerk } from '@clerk/clerk-react';
interface UserBadgeProps {
colorMode?: 'light' | 'dark';
}
const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
const { user, isSignedIn } = useUser();
const { signOut } = useClerk();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const initials = React.useMemo(() => {
const first = user?.firstName?.[0] || '';
const last = user?.lastName?.[0] || '';
return (first + last || user?.username?.[0] || user?.primaryEmailAddress?.emailAddress?.[0] || '?').toUpperCase();
}, [user]);
if (!isSignedIn) return null;
const handleOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
const handleClose = () => setAnchorEl(null);
const handleSignOut = async () => {
try {
await signOut();
} finally {
window.location.assign('/');
}
};
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title={`${user?.fullName || user?.username || user?.primaryEmailAddress?.emailAddress || 'User'}`}>
<Avatar
onClick={handleOpen}
sx={{
width: 36,
height: 36,
cursor: 'pointer',
bgcolor: colorMode === 'dark' ? 'rgba(255,255,255,0.2)' : 'primary.main',
color: colorMode === 'dark' ? 'white' : 'white',
fontWeight: 700,
}}
src={user?.imageUrl || undefined}
>
{initials}
</Avatar>
</Tooltip>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
<Box sx={{ px: 2, py: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{user?.fullName || user?.username || 'User'}
</Typography>
<Typography variant="caption" color="text.secondary">
{user?.primaryEmailAddress?.emailAddress}
</Typography>
</Box>
<MenuItem onClick={handleClose}>Signed in</MenuItem>
<MenuItem onClick={handleSignOut}>Sign out</MenuItem>
</Menu>
</Box>
);
};
export default UserBadge;