Refactor components and modals

This commit is contained in:
Urtzi Alfaro
2025-09-26 07:46:25 +02:00
parent cf4405b771
commit d573c38621
80 changed files with 3421 additions and 4617 deletions

View 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;

View File

@@ -0,0 +1,2 @@
export { default as ErrorBoundary } from './ErrorBoundary';
export type { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary';

View File

@@ -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

View File

@@ -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';