Refactor components and modals
This commit is contained in:
299
frontend/src/components/layout/ErrorBoundary/ErrorBoundary.tsx
Normal file
299
frontend/src/components/layout/ErrorBoundary/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { Button } from '../../ui';
|
||||
|
||||
export interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
/** Componentes hijos */
|
||||
children: ReactNode;
|
||||
/** Componente de fallback personalizado */
|
||||
fallback?: (error: Error, errorInfo: ErrorInfo, retry: () => void) => ReactNode;
|
||||
/** Función llamada cuando ocurre un error */
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
/** Mostrar información detallada del error en desarrollo */
|
||||
showDetails?: boolean;
|
||||
/** Título personalizado del error */
|
||||
title?: string;
|
||||
/** Descripción personalizada del error */
|
||||
description?: string;
|
||||
/** Habilitar botón de recarga */
|
||||
enableReload?: boolean;
|
||||
/** Habilitar botón de reintentar */
|
||||
enableRetry?: boolean;
|
||||
/** Habilitar navegación hacia atrás */
|
||||
enableGoBack?: boolean;
|
||||
/** Función de retry personalizada */
|
||||
onRetry?: () => void;
|
||||
/** Función de navegación hacia atrás personalizada */
|
||||
onGoBack?: () => void;
|
||||
/** Función de reporte de errores */
|
||||
onReportError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
/** Clase CSS adicional para el contenedor de error */
|
||||
className?: string;
|
||||
/** Nivel de logging */
|
||||
logLevel?: 'none' | 'console' | 'service';
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
private retryTimeoutId: number | null = null;
|
||||
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
|
||||
// Log del error
|
||||
this.logError(error, errorInfo);
|
||||
|
||||
// Callback opcional para manejo de errores
|
||||
if (this.props.onError) {
|
||||
this.props.onError(error, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private logError = (error: Error, errorInfo: ErrorInfo) => {
|
||||
const { logLevel = 'console' } = this.props;
|
||||
|
||||
if (logLevel === 'none') return;
|
||||
|
||||
const errorData = {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent,
|
||||
url: window.location.href,
|
||||
};
|
||||
|
||||
if (logLevel === 'console' || process.env.NODE_ENV === 'development') {
|
||||
console.group('🚨 Error Boundary');
|
||||
console.error('Error:', error);
|
||||
console.error('Error Info:', errorInfo);
|
||||
console.error('Error Data:', errorData);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Aquí se podría integrar con un servicio de logging como Sentry
|
||||
if (logLevel === 'service' && process.env.NODE_ENV === 'production') {
|
||||
// Ejemplo: Sentry.captureException(error, { extra: errorData });
|
||||
}
|
||||
};
|
||||
|
||||
private handleRetry = () => {
|
||||
if (this.props.onRetry) {
|
||||
this.props.onRetry();
|
||||
} else {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
private handleGoBack = () => {
|
||||
if (this.props.onGoBack) {
|
||||
this.props.onGoBack();
|
||||
} else if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
};
|
||||
|
||||
private handleReportError = () => {
|
||||
const { error, errorInfo } = this.state;
|
||||
if (error && errorInfo && this.props.onReportError) {
|
||||
this.props.onReportError(error, errorInfo);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.retryTimeoutId) {
|
||||
window.clearTimeout(this.retryTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError, error, errorInfo } = this.state;
|
||||
const {
|
||||
children,
|
||||
fallback,
|
||||
title = 'Oops! Algo salió mal',
|
||||
description = 'Ha ocurrido un error inesperado en la aplicación. Nuestro equipo ha sido notificado.',
|
||||
showDetails = process.env.NODE_ENV === 'development',
|
||||
enableRetry = true,
|
||||
enableReload = true,
|
||||
enableGoBack = true,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
if (!hasError) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Si hay un componente de fallback personalizado
|
||||
if (fallback && error && errorInfo) {
|
||||
return fallback(error, errorInfo, this.handleRetry);
|
||||
}
|
||||
|
||||
// UI de error por defecto
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'min-h-[400px] flex items-center justify-center p-6',
|
||||
className
|
||||
)}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<div className="max-w-md w-full text-center">
|
||||
{/* Icono de error */}
|
||||
<div className="mb-6">
|
||||
<svg
|
||||
className="w-20 h-20 text-color-error mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Título */}
|
||||
<h2 className="text-2xl font-semibold text-text-primary mb-4">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Descripción */}
|
||||
<p className="text-text-secondary mb-6 leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Detalles del error (solo en desarrollo) */}
|
||||
{showDetails && error && (
|
||||
<details className="mb-6 text-left bg-bg-tertiary rounded-lg p-4">
|
||||
<summary className="cursor-pointer font-medium text-color-error mb-2">
|
||||
Detalles técnicos
|
||||
</summary>
|
||||
<div className="space-y-2 text-sm text-text-secondary font-mono">
|
||||
<div>
|
||||
<strong>Error:</strong> {error.message}
|
||||
</div>
|
||||
{error.stack && (
|
||||
<div>
|
||||
<strong>Stack:</strong>
|
||||
<pre className="mt-1 overflow-auto max-h-40 bg-bg-quaternary p-2 rounded text-xs">
|
||||
{error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{errorInfo?.componentStack && (
|
||||
<div>
|
||||
<strong>Component Stack:</strong>
|
||||
<pre className="mt-1 overflow-auto max-h-40 bg-bg-quaternary p-2 rounded text-xs">
|
||||
{errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Acciones */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
{enableRetry && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={this.handleRetry}
|
||||
leftIcon={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{enableReload && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={this.handleReload}
|
||||
leftIcon={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
Recargar página
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{enableGoBack && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={this.handleGoBack}
|
||||
leftIcon={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
Volver atrás
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reportar error */}
|
||||
{this.props.onReportError && (
|
||||
<div className="mt-6 pt-6 border-t border-border-primary">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={this.handleReportError}
|
||||
className="text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
📋 Reportar este error
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
2
frontend/src/components/layout/ErrorBoundary/index.ts
Normal file
2
frontend/src/components/layout/ErrorBoundary/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||
export type { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary';
|
||||
@@ -1,70 +0,0 @@
|
||||
# MinimalSidebar Component
|
||||
|
||||
A minimalist, responsive sidebar component for the Panadería IA application, inspired by grok.com's clean design.
|
||||
|
||||
## Features
|
||||
|
||||
- **Minimalist Design**: Clean, uncluttered interface following modern UI principles
|
||||
- **Responsive**: Works on both desktop and mobile devices
|
||||
- **Collapsible**: Can be collapsed on desktop to save space
|
||||
- **Navigation Hierarchy**: Supports nested menu items with expand/collapse functionality
|
||||
- **Profile Integration**: Includes user profile section with logout functionality
|
||||
- **Theme Consistency**: Follows the application's global color palette and design system
|
||||
- **Accessibility**: Proper ARIA labels and keyboard navigation support
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { MinimalSidebar } from './MinimalSidebar';
|
||||
|
||||
// Basic usage
|
||||
<MinimalSidebar />
|
||||
|
||||
// With custom props
|
||||
<MinimalSidebar
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
onToggleCollapse={toggleSidebar}
|
||||
isOpen={isMobileMenuOpen}
|
||||
onClose={closeMobileMenu}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `className` | `string` | Additional CSS classes |
|
||||
| `isOpen` | `boolean` | Whether the mobile drawer is open |
|
||||
| `isCollapsed` | `boolean` | Whether the desktop sidebar is collapsed |
|
||||
| `onClose` | `() => void` | Callback when sidebar is closed (mobile) |
|
||||
| `onToggleCollapse` | `() => void` | Callback when collapse state changes (desktop) |
|
||||
| `customItems` | `NavigationItem[]` | Custom navigation items |
|
||||
| `showCollapseButton` | `boolean` | Whether to show the collapse button |
|
||||
| `showFooter` | `boolean` | Whether to show the footer section |
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **Minimalist Aesthetic**: Clean lines, ample whitespace, and focused content
|
||||
- **Grok.com Inspired**: Follows the clean, functional design of grok.com
|
||||
- **Consistent with Brand**: Uses the application's color palette and typography
|
||||
- **Mobile First**: Responsive design that works well on all screen sizes
|
||||
- **Performance Focused**: Lightweight implementation with minimal dependencies
|
||||
|
||||
## Color Palette
|
||||
|
||||
The component uses the application's global CSS variables for consistent theming:
|
||||
|
||||
- `--color-primary`: Primary brand color (orange)
|
||||
- `--color-secondary`: Secondary brand color (green)
|
||||
- `--bg-primary`: Main background color
|
||||
- `--bg-secondary`: Secondary background color
|
||||
- `--text-primary`: Primary text color
|
||||
- `--text-secondary`: Secondary text color
|
||||
- `--border-primary`: Primary border color
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Proper ARIA attributes for screen readers
|
||||
- Keyboard navigation support
|
||||
- Focus management
|
||||
- Semantic HTML structure
|
||||
@@ -7,6 +7,7 @@ export { PageHeader } from './PageHeader';
|
||||
export { Footer } from './Footer';
|
||||
export { PublicHeader } from './PublicHeader';
|
||||
export { PublicLayout } from './PublicLayout';
|
||||
export { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
// Export types
|
||||
export type { AppShellProps } from './AppShell';
|
||||
@@ -16,4 +17,5 @@ export type { BreadcrumbsProps, BreadcrumbItem } from './Breadcrumbs';
|
||||
export type { PageHeaderProps } from './PageHeader';
|
||||
export type { FooterProps } from './Footer';
|
||||
export type { PublicHeaderProps, PublicHeaderRef } from './PublicHeader';
|
||||
export type { PublicLayoutProps, PublicLayoutRef } from './PublicLayout';
|
||||
export type { PublicLayoutProps, PublicLayoutRef } from './PublicLayout';
|
||||
export type { ErrorBoundaryProps } from './ErrorBoundary';
|
||||
Reference in New Issue
Block a user