ADD new frontend
This commit is contained in:
261
frontend/src/components/layout/AppShell/AppShell.tsx
Normal file
261
frontend/src/components/layout/AppShell/AppShell.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useCallback, forwardRef } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useAuthUser, useIsAuthenticated } from '../../../stores';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { Header } from '../Header';
|
||||
import { Sidebar } from '../Sidebar';
|
||||
import { Footer } from '../Footer';
|
||||
|
||||
export interface AppShellProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
/**
|
||||
* Control sidebar visibility
|
||||
*/
|
||||
showSidebar?: boolean;
|
||||
/**
|
||||
* Control header visibility
|
||||
*/
|
||||
showHeader?: boolean;
|
||||
/**
|
||||
* Control footer visibility
|
||||
*/
|
||||
showFooter?: boolean;
|
||||
/**
|
||||
* Make the layout fullscreen (hide header and footer)
|
||||
*/
|
||||
fullScreen?: boolean;
|
||||
/**
|
||||
* Add padding to the main content area
|
||||
*/
|
||||
padded?: boolean;
|
||||
/**
|
||||
* Initial sidebar collapsed state
|
||||
*/
|
||||
initialSidebarCollapsed?: boolean;
|
||||
/**
|
||||
* Custom loading component
|
||||
*/
|
||||
loadingComponent?: React.ReactNode;
|
||||
/**
|
||||
* Custom error boundary
|
||||
*/
|
||||
errorBoundary?: React.ComponentType<{ children: React.ReactNode; error?: Error }>;
|
||||
}
|
||||
|
||||
export interface AppShellRef {
|
||||
toggleSidebar: () => void;
|
||||
collapseSidebar: () => void;
|
||||
expandSidebar: () => void;
|
||||
isSidebarOpen: boolean;
|
||||
isSidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* AppShell - Main application shell that combines header, sidebar, and main content area
|
||||
*
|
||||
* Features:
|
||||
* - Responsive layout with collapsible sidebar
|
||||
* - Mobile-friendly with drawer overlay
|
||||
* - Content area that adjusts to sidebar state
|
||||
* - Supports fullscreen mode for special pages
|
||||
* - Integrated with authentication and theming
|
||||
*/
|
||||
export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
|
||||
children,
|
||||
className,
|
||||
showSidebar = true,
|
||||
showHeader = true,
|
||||
showFooter = true,
|
||||
fullScreen = false,
|
||||
padded = true,
|
||||
initialSidebarCollapsed = false,
|
||||
loadingComponent,
|
||||
errorBoundary: ErrorBoundary,
|
||||
}, ref) => {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const authLoading = false; // Since we're in a protected route, auth loading should be false
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(initialSidebarCollapsed);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Sidebar control functions
|
||||
const toggleSidebar = useCallback(() => {
|
||||
if (window.innerWidth < 1024) {
|
||||
// Mobile: toggle drawer
|
||||
setIsSidebarOpen(prev => !prev);
|
||||
} else {
|
||||
// Desktop: toggle collapsed state
|
||||
setIsSidebarCollapsed(prev => !prev);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const collapseSidebar = useCallback(() => {
|
||||
if (window.innerWidth < 1024) {
|
||||
setIsSidebarOpen(false);
|
||||
} else {
|
||||
setIsSidebarCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const expandSidebar = useCallback(() => {
|
||||
if (window.innerWidth < 1024) {
|
||||
setIsSidebarOpen(true);
|
||||
} else {
|
||||
setIsSidebarCollapsed(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Close mobile sidebar when clicking outside
|
||||
const handleOverlayClick = useCallback(() => {
|
||||
if (window.innerWidth < 1024) {
|
||||
setIsSidebarOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Expose ref methods
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
toggleSidebar,
|
||||
collapseSidebar,
|
||||
expandSidebar,
|
||||
isSidebarOpen,
|
||||
isSidebarCollapsed,
|
||||
}), [toggleSidebar, collapseSidebar, expandSidebar, isSidebarOpen, isSidebarCollapsed]);
|
||||
|
||||
// Handle responsive sidebar state
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
// Desktop: close mobile drawer
|
||||
setIsSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Error boundary handling
|
||||
React.useEffect(() => {
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
setError(new Error(event.message));
|
||||
};
|
||||
|
||||
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
||||
setError(new Error(event.reason?.message || 'Promise rejection'));
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleError);
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('error', handleError);
|
||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Show loading state
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[var(--bg-primary)]">
|
||||
{loadingComponent || (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
|
||||
<p className="text-[var(--text-secondary)]">Cargando aplicación...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error && ErrorBoundary) {
|
||||
return <ErrorBoundary error={error}>{children}</ErrorBoundary>;
|
||||
}
|
||||
|
||||
const shouldShowSidebar = showSidebar && isAuthenticated && !fullScreen;
|
||||
const shouldShowHeader = showHeader && !fullScreen;
|
||||
const shouldShowFooter = showFooter && !fullScreen;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'min-h-screen bg-[var(--bg-primary)] flex flex-col',
|
||||
resolvedTheme,
|
||||
className
|
||||
)}
|
||||
data-testid="app-shell"
|
||||
>
|
||||
{/* Header */}
|
||||
{shouldShowHeader && (
|
||||
<Header
|
||||
onMenuClick={toggleSidebar}
|
||||
sidebarCollapsed={isSidebarCollapsed}
|
||||
className="z-[var(--z-fixed)]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 relative">
|
||||
{/* Sidebar */}
|
||||
{shouldShowSidebar && (
|
||||
<>
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
className="z-[var(--z-fixed)]"
|
||||
/>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-[var(--z-modal-backdrop)] lg:hidden"
|
||||
onClick={handleOverlayClick}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main
|
||||
className={clsx(
|
||||
'flex-1 flex flex-col transition-all duration-300 ease-in-out',
|
||||
// Adjust margins based on sidebar state
|
||||
shouldShowSidebar && isAuthenticated && {
|
||||
'lg:ml-[var(--sidebar-width)]': !isSidebarCollapsed,
|
||||
'lg:ml-16': isSidebarCollapsed,
|
||||
},
|
||||
// Add header offset
|
||||
shouldShowHeader && 'pt-[var(--header-height)]',
|
||||
// Add padding to content
|
||||
padded && 'p-4 lg:p-6'
|
||||
)}
|
||||
role="main"
|
||||
aria-label="Contenido principal"
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{shouldShowFooter && (
|
||||
<Footer
|
||||
compact={true}
|
||||
showPrivacyLinks={true}
|
||||
className={clsx(
|
||||
'transition-all duration-300 ease-in-out',
|
||||
shouldShowSidebar && isAuthenticated && {
|
||||
'lg:ml-[var(--sidebar-width)]': !isSidebarCollapsed,
|
||||
'lg:ml-16': isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AppShell.displayName = 'AppShell';
|
||||
Reference in New Issue
Block a user