1188 lines
32 KiB
Markdown
1188 lines
32 KiB
Markdown
|
|
# 🎨 ANÁLISIS UX/UI Y FRONTEND
|
||
|
|
|
||
|
|
**Fecha:** 2025-01-07
|
||
|
|
**Analista:** Experto UX/UI + Desarrollador Frontend
|
||
|
|
**Versión del Proyecto:** 1.0.0
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 RESUMEN EJECUTIVO
|
||
|
|
|
||
|
|
| Aspecto | Estado | Calificación |
|
||
|
|
|---------|--------|--------------|
|
||
|
|
| **Componentes UI** | ✅ Bueno (shadcn/ui) | 7.5/10 |
|
||
|
|
| **Reutilización** | ⚠️ Mejorable | 6/10 |
|
||
|
|
| **Accesibilidad** | ⚠️ Básica | 5/10 |
|
||
|
|
| **Responsividad** | ✅ Buena | 8/10 |
|
||
|
|
| **Rendimiento** | ✅ Optimizado (lazy loading) | 7.5/10 |
|
||
|
|
| **Flujos de Usuario** | ⚠️ Algunos puntos de fricción | 6.5/10 |
|
||
|
|
| **Estados de Carga/Error** | ⚠️ Inconsistentes | 6/10 |
|
||
|
|
|
||
|
|
**Calificación General: 6.6/10** ⚠️ **BUENO CON MEJORAS NECESARIAS**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 🔍 ANÁLISIS DE COMPONENTES UI
|
||
|
|
|
||
|
|
### 1.1 Reutilización de Componentes
|
||
|
|
|
||
|
|
#### ✅ Componentes Bien Reutilizados
|
||
|
|
|
||
|
|
| Componente | Usos | Estado | Ubicación |
|
||
|
|
|------------|------|--------|-----------|
|
||
|
|
| **Badge** | 50+ | ✅ Excelente | `components/shared/Badge.tsx` |
|
||
|
|
| **Button** | 100+ | ✅ Excelente | `components/ui/button.tsx` |
|
||
|
|
| **Card** | 30+ | ✅ Bueno | `components/ui/card.tsx` |
|
||
|
|
| **EmergencyButton** | 10+ | ✅ Bueno | `components/shared/EmergencyButton.tsx` |
|
||
|
|
| **MarkdownViewer** | 15+ | ✅ Bueno | `components/content/MarkdownViewer.tsx` |
|
||
|
|
|
||
|
|
#### ⚠️ Componentes con Duplicación o Inconsistencias
|
||
|
|
|
||
|
|
| Problema | Ubicaciones | Impacto | Prioridad |
|
||
|
|
|----------|-------------|---------|-----------|
|
||
|
|
| **Card Variants Duplicados** | `ProcedureCard`, `DrugCard`, `GuideCard` | 🟡 Media | Media |
|
||
|
|
| **Loading States Inconsistentes** | Múltiples páginas | 🟡 Media | Media |
|
||
|
|
| **Error States Inconsistentes** | `ManualViewer`, `NotFound`, otros | 🟡 Media | Media |
|
||
|
|
| **Button Styles Inconsistentes** | Varios componentes | 🟡 Baja | Baja |
|
||
|
|
|
||
|
|
#### ❌ Problemas Identificados
|
||
|
|
|
||
|
|
**1. Card Components con Lógica Similar pero No Unificada**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: ProcedureCard.tsx (198 líneas)
|
||
|
|
// Lógica de expand/collapse, favoritos, compartir duplicada
|
||
|
|
|
||
|
|
// ❌ PROBLEMA: DrugCard.tsx
|
||
|
|
// Misma lógica pero implementación diferente
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Crear componente base <BaseCard>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Archivos afectados:**
|
||
|
|
- `src/components/procedures/ProcedureCard.tsx` (198 líneas)
|
||
|
|
- `src/components/drugs/DrugCard.tsx`
|
||
|
|
- `src/components/guide/GuideCard.tsx`
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ Crear: src/components/shared/BaseCard.tsx
|
||
|
|
interface BaseCardProps {
|
||
|
|
title: string;
|
||
|
|
subtitle?: string;
|
||
|
|
badges?: BadgeProps[];
|
||
|
|
expandable?: boolean;
|
||
|
|
defaultExpanded?: boolean;
|
||
|
|
onFavorite?: () => void;
|
||
|
|
onShare?: () => void;
|
||
|
|
children?: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const BaseCard = ({
|
||
|
|
title,
|
||
|
|
subtitle,
|
||
|
|
badges,
|
||
|
|
expandable,
|
||
|
|
defaultExpanded = false,
|
||
|
|
onFavorite,
|
||
|
|
onShare,
|
||
|
|
children
|
||
|
|
}: BaseCardProps) => {
|
||
|
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||
|
|
// ... lógica unificada
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Loading States Inconsistentes**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: MarkdownViewer.tsx (línea 84-90)
|
||
|
|
if (loading && showLoading) {
|
||
|
|
return (
|
||
|
|
<div className={`flex flex-col items-center justify-center py-12 ${className}`}>
|
||
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary mb-4" />
|
||
|
|
<p className="text-muted-foreground">Cargando contenido...</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ PROBLEMA: ManualViewer.tsx (línea 32-45)
|
||
|
|
// Loading state diferente (solo "Capítulo no encontrado")
|
||
|
|
// Sin estado de carga real
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Crear componente <LoadingState>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ Crear: src/components/shared/LoadingState.tsx
|
||
|
|
interface LoadingStateProps {
|
||
|
|
message?: string;
|
||
|
|
size?: 'sm' | 'md' | 'lg';
|
||
|
|
fullScreen?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const LoadingState = ({
|
||
|
|
message = 'Cargando...',
|
||
|
|
size = 'md',
|
||
|
|
fullScreen = false
|
||
|
|
}: LoadingStateProps) => {
|
||
|
|
const sizeClasses = {
|
||
|
|
sm: 'w-4 h-4',
|
||
|
|
md: 'w-8 h-8',
|
||
|
|
lg: 'w-12 h-12'
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`flex flex-col items-center justify-center ${
|
||
|
|
fullScreen ? 'min-h-[60vh]' : 'py-12'
|
||
|
|
}`}>
|
||
|
|
<Loader2 className={`${sizeClasses[size]} animate-spin text-primary mb-4`} />
|
||
|
|
<p className="text-muted-foreground">{message}</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Error States Inconsistentes**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: NotFound.tsx (línea 11-21)
|
||
|
|
// Error state sin accesibilidad, sin navegación clara
|
||
|
|
|
||
|
|
// ❌ PROBLEMA: ManualViewer.tsx (línea 32-45)
|
||
|
|
// Error state diferente (usa "Capítulo no encontrado")
|
||
|
|
|
||
|
|
// ❌ PROBLEMA: MarkdownViewer.tsx (línea 94-102)
|
||
|
|
// Error state con más detalles pero sin acciones claras
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ Crear: src/components/shared/ErrorState.tsx
|
||
|
|
interface ErrorStateProps {
|
||
|
|
title?: string;
|
||
|
|
message: string;
|
||
|
|
action?: {
|
||
|
|
label: string;
|
||
|
|
onClick: () => void;
|
||
|
|
};
|
||
|
|
icon?: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ErrorState = ({
|
||
|
|
title = 'Error',
|
||
|
|
message,
|
||
|
|
action,
|
||
|
|
icon = <AlertCircle className="w-12 h-12 text-destructive" />
|
||
|
|
}: ErrorStateProps) => {
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4" role="alert">
|
||
|
|
{icon}
|
||
|
|
<h2 className="text-xl font-semibold text-foreground">{title}</h2>
|
||
|
|
<p className="text-muted-foreground text-center max-w-md">{message}</p>
|
||
|
|
{action && (
|
||
|
|
<Button onClick={action.onClick}>
|
||
|
|
{action.label}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.2 Responsividad y Breakpoints
|
||
|
|
|
||
|
|
#### ✅ Breakpoints Implementados
|
||
|
|
|
||
|
|
| Breakpoint | Valor | Uso | Estado |
|
||
|
|
|------------|-------|-----|--------|
|
||
|
|
| **Mobile** | < 768px | Hook `useIsMobile()` | ✅ Funcional |
|
||
|
|
| **Tablet** | 768px - 1024px | Tailwind `md:` | ✅ Implementado |
|
||
|
|
| **Desktop** | > 1024px | Tailwind `lg:`, `xl:`, `2xl:` | ✅ Implementado |
|
||
|
|
|
||
|
|
#### ⚠️ Problemas de Responsividad
|
||
|
|
|
||
|
|
**1. Hook `useIsMobile()` con Problema de Hydration**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: src/hooks/use-mobile.tsx (línea 6)
|
||
|
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: undefined inicial causa hydration mismatch en SSR
|
||
|
|
// ⚠️ PROBLEMA: window.innerWidth no está disponible en SSR
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: src/hooks/use-mobile.tsx
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
|
||
|
|
const MOBILE_BREAKPOINT = 768;
|
||
|
|
|
||
|
|
export function useIsMobile() {
|
||
|
|
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
// Solo ejecutar en cliente
|
||
|
|
if (typeof window === 'undefined') return;
|
||
|
|
|
||
|
|
const checkMobile = () => {
|
||
|
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Check inicial
|
||
|
|
checkMobile();
|
||
|
|
|
||
|
|
// Media query listener (más eficiente que resize)
|
||
|
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||
|
|
const handleChange = (e: MediaQueryListEvent) => {
|
||
|
|
setIsMobile(e.matches);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Modern API
|
||
|
|
if (mql.addEventListener) {
|
||
|
|
mql.addEventListener('change', handleChange);
|
||
|
|
return () => mql.removeEventListener('change', handleChange);
|
||
|
|
} else {
|
||
|
|
// Fallback para navegadores antiguos
|
||
|
|
mql.addListener(handleChange);
|
||
|
|
return () => mql.removeListener(handleChange);
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return isMobile;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Grids sin Breakpoints Específicos**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Index.tsx (línea 52)
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
{/* Emergency Buttons */}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: grid-cols-2 siempre, incluso en mobile pequeño
|
||
|
|
// Debería ser grid-cols-1 en mobile muy pequeño
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Responsive grid
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
|
|
{/* Emergency Buttons */}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Floating Button Sin Consideración Mobile**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Index.tsx (línea 151-157)
|
||
|
|
<Link
|
||
|
|
to="/rcp"
|
||
|
|
className="fixed bottom-24 right-4 z-40 w-16 h-16 rounded-full bg-primary flex items-center justify-center shadow-lg animate-pulse-ring"
|
||
|
|
aria-label="Emergencia - RCP"
|
||
|
|
>
|
||
|
|
<AlertTriangle className="w-8 h-8 text-primary-foreground" />
|
||
|
|
</Link>
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: Puede taparse con BottomNav en mobile
|
||
|
|
// ⚠️ PROBLEMA: No considera safe-area-inset-bottom
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Safe area aware floating button
|
||
|
|
<Link
|
||
|
|
to="/rcp"
|
||
|
|
className="fixed bottom-[calc(4rem+env(safe-area-inset-bottom))] right-4 z-40 w-16 h-16 rounded-full bg-primary flex items-center justify-center shadow-lg animate-pulse-ring touch-manipulation"
|
||
|
|
aria-label="Emergencia - RCP"
|
||
|
|
style={{ bottom: 'calc(4rem + env(safe-area-inset-bottom, 0px))' }}
|
||
|
|
>
|
||
|
|
<AlertTriangle className="w-8 h-8 text-primary-foreground" />
|
||
|
|
</Link>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 Accesibilidad (ARIA, Contraste, Navegación por Teclado)
|
||
|
|
|
||
|
|
#### ✅ Buenas Prácticas Implementadas
|
||
|
|
|
||
|
|
| Práctica | Estado | Ejemplo |
|
||
|
|
|----------|--------|---------|
|
||
|
|
| **aria-expanded** | ✅ Implementado | `ProcedureCard.tsx` línea 76 |
|
||
|
|
| **aria-label** | ✅ Implementado | Botones de acción (línea 99, 109) |
|
||
|
|
| **Loading lazy en imágenes** | ✅ Implementado | `MarkdownViewer.tsx` línea 324, 345 |
|
||
|
|
| **Enlaces externos con rel** | ✅ Implementado | `MarkdownViewer.tsx` línea 259 |
|
||
|
|
|
||
|
|
#### ❌ Problemas Críticos de Accesibilidad
|
||
|
|
|
||
|
|
**1. Falta de ARIA Labels en Elementos Interactivos**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Index.tsx (línea 37-45)
|
||
|
|
<button
|
||
|
|
onClick={onSearchClick}
|
||
|
|
className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors"
|
||
|
|
>
|
||
|
|
{/* ⚠️ PROBLEMA: Sin aria-label, sin type="button" */}
|
||
|
|
<Search className="w-6 h-6 text-muted-foreground" />
|
||
|
|
<span className="text-muted-foreground">
|
||
|
|
Buscar protocolo, fármaco, calculadora...
|
||
|
|
</span>
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Añadir aria-label y type
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onSearchClick}
|
||
|
|
className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||
|
|
aria-label="Abrir búsqueda de protocolos, fármacos y calculadoras"
|
||
|
|
>
|
||
|
|
<Search className="w-6 h-6 text-muted-foreground" aria-hidden="true" />
|
||
|
|
<span className="text-muted-foreground">
|
||
|
|
Buscar protocolo, fármaco, calculadora...
|
||
|
|
</span>
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Falta de Focus Visible**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Muchos botones sin focus visible
|
||
|
|
// No hay estilos focus en Tailwind config
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir focus ring a todos los botones
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix en Tailwind Config:**
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// ✅ FIX: tailwind.config.ts
|
||
|
|
extend: {
|
||
|
|
// ... existing config
|
||
|
|
ringOffsetWidth: {
|
||
|
|
DEFAULT: '2px',
|
||
|
|
},
|
||
|
|
// Asegurar que focus:outline-none está en base
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Añadir a index.css:**
|
||
|
|
|
||
|
|
```css
|
||
|
|
/* ✅ FIX: Focus visible para todos los elementos interactivos */
|
||
|
|
*:focus-visible {
|
||
|
|
outline: 2px solid hsl(var(--primary));
|
||
|
|
outline-offset: 2px;
|
||
|
|
border-radius: calc(var(--radius) - 2px);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Imágenes Sin Alt Text Consistente**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: MarkdownViewer.tsx (línea 344)
|
||
|
|
alt={imageAlt || 'Imagen'}
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: 'Imagen' no es descriptivo
|
||
|
|
// ⚠️ PROBLEMA: No hay validación de alt text en Markdown
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Mejorar alt text fallback
|
||
|
|
alt={imageAlt || imageCaption || `Imagen en ${filePath.split('/').pop()}`}
|
||
|
|
```
|
||
|
|
|
||
|
|
**4. Falta de Skip Links**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: No hay skip links para navegación por teclado
|
||
|
|
// Usuarios de screen readers deben tabular por toda la navegación
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir skip link al inicio de App.tsx
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Añadir en App.tsx después del ErrorBoundary
|
||
|
|
<div className="min-h-screen bg-background flex flex-col">
|
||
|
|
{/* Skip to main content link */}
|
||
|
|
<a
|
||
|
|
href="#main-content"
|
||
|
|
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg"
|
||
|
|
>
|
||
|
|
Saltar al contenido principal
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<Header ... />
|
||
|
|
|
||
|
|
<main id="main-content" className="pt-14 pb-safe flex-1">
|
||
|
|
{/* ... */}
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**5. Contraste de Colores**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: No hay verificación de contraste WCAG AA
|
||
|
|
// Colores definidos en HSL pueden no cumplir contraste mínimo
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Usar herramienta de verificación
|
||
|
|
// Recomendación: @storybook/addon-a11y o pa11y
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.4 Inconsistencias Visuales
|
||
|
|
|
||
|
|
**1. Espaciado Inconsistente**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Diferentes sistemas de espaciado
|
||
|
|
// Index.tsx: space-y-6
|
||
|
|
// ManualViewer.tsx: space-y-6
|
||
|
|
// RCP.tsx: espacio inconsistente
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Usar sistema de diseño consistente
|
||
|
|
const spacing = {
|
||
|
|
section: 'space-y-6',
|
||
|
|
card: 'space-y-3',
|
||
|
|
list: 'space-y-2',
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Typography Scale Inconsistente**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: Diferentes tamaños de fuente sin sistema
|
||
|
|
// h1: text-2xl, text-3xl, text-4xl (inconsistente)
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Usar sistema de tipografía
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 🔄 FLUJOS DE USUARIO EN CÓDIGO
|
||
|
|
|
||
|
|
### 2.1 Flujo Principal: Búsqueda → Resultado → Detalle
|
||
|
|
|
||
|
|
**Mapeo del Flujo:**
|
||
|
|
|
||
|
|
```
|
||
|
|
1. Usuario abre app (Home)
|
||
|
|
↓
|
||
|
|
2. Click en Search Bar (Index.tsx línea 37)
|
||
|
|
↓
|
||
|
|
3. Abre SearchModal (SearchModal.tsx)
|
||
|
|
↓
|
||
|
|
4. Escribe query (debounce implícito, línea 54-58)
|
||
|
|
↓
|
||
|
|
5. Muestra resultados (línea 60-118)
|
||
|
|
↓
|
||
|
|
6. Click en resultado → Navigate (línea 137-146)
|
||
|
|
↓
|
||
|
|
7. Página de detalle (ProcedureCard, DrugCard, etc.)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### ⚠️ Puntos de Fricción Identificados
|
||
|
|
|
||
|
|
**1. Sin Debounce en Búsqueda**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: SearchModal.tsx (línea 54-58)
|
||
|
|
useEffect(() => {
|
||
|
|
if (query.length < 2) {
|
||
|
|
setResults([]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Búsqueda inmediata sin debounce
|
||
|
|
// Puede causar múltiples renders innecesarios
|
||
|
|
}, [query, typeFilter, categoryFilter]);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Añadir debounce
|
||
|
|
import { useDebouncedValue } from '@/hooks/useDebounce';
|
||
|
|
|
||
|
|
const SearchModal = ({ isOpen, onClose }: SearchModalProps) => {
|
||
|
|
const [query, setQuery] = useState('');
|
||
|
|
const debouncedQuery = useDebouncedValue(query, 300); // 300ms debounce
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (debouncedQuery.length < 2) {
|
||
|
|
setResults([]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Búsqueda con debouncedQuery
|
||
|
|
}, [debouncedQuery, typeFilter, categoryFilter]);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Navegación Sin Estado de Carga**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Al hacer click en resultado, no hay feedback inmediato
|
||
|
|
// El usuario no sabe si está cargando o si hay error
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir loading state en navigate
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Añadir loading state
|
||
|
|
const handleResultClick = (result: SearchResult) => {
|
||
|
|
setResults([]); // Limpiar resultados
|
||
|
|
addToHistory({ ...result, path: result.path });
|
||
|
|
navigate(result.path);
|
||
|
|
onClose(); // Cerrar modal
|
||
|
|
};
|
||
|
|
|
||
|
|
// En el botón de resultado:
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setIsNavigating(true);
|
||
|
|
handleResultClick(result);
|
||
|
|
}}
|
||
|
|
disabled={isNavigating}
|
||
|
|
className={cn(
|
||
|
|
"flex items-center gap-3 p-3 rounded-lg transition-colors",
|
||
|
|
isNavigating && "opacity-50 cursor-not-allowed"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{isNavigating ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowRight />}
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Sin Historial de Búsqueda Visible en Modal**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: Historial solo visible en Home, no en SearchModal
|
||
|
|
// Usuario debe cerrar modal y volver a Home para ver historial
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Mostrar historial reciente en SearchModal cuando está vacío
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 Flujo: Onboarding / Primera Visita
|
||
|
|
|
||
|
|
**Estado Actual:** ❌ **NO EXISTE**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: No hay onboarding para nuevos usuarios
|
||
|
|
// No hay explicación de features
|
||
|
|
// No hay tour de la app
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Crear componente de onboarding
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ CREAR: src/components/onboarding/OnboardingFlow.tsx
|
||
|
|
interface OnboardingStep {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
description: string;
|
||
|
|
target: string; // CSS selector
|
||
|
|
position: 'top' | 'bottom' | 'left' | 'right';
|
||
|
|
}
|
||
|
|
|
||
|
|
export const OnboardingFlow = () => {
|
||
|
|
const [currentStep, setCurrentStep] = useState(0);
|
||
|
|
const [isComplete, setIsComplete] = useState(
|
||
|
|
localStorage.getItem('onboarding-complete') === 'true'
|
||
|
|
);
|
||
|
|
|
||
|
|
if (isComplete) return null;
|
||
|
|
|
||
|
|
const steps: OnboardingStep[] = [
|
||
|
|
{
|
||
|
|
id: 'search',
|
||
|
|
title: 'Busca rápidamente',
|
||
|
|
description: 'Usa la barra de búsqueda para encontrar protocolos, fármacos o calculadoras',
|
||
|
|
target: '[aria-label="Abrir búsqueda"]',
|
||
|
|
position: 'bottom',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'emergency',
|
||
|
|
title: 'Emergencias críticas',
|
||
|
|
description: 'Acceso rápido a los protocolos más importantes',
|
||
|
|
target: '[aria-label="Emergencia - RCP"]',
|
||
|
|
position: 'left',
|
||
|
|
},
|
||
|
|
// ... más pasos
|
||
|
|
];
|
||
|
|
|
||
|
|
// Implementar overlay y tooltip
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 Flujo: Navegación Nested (Manual, Guías)
|
||
|
|
|
||
|
|
**Mapeo del Flujo:**
|
||
|
|
|
||
|
|
```
|
||
|
|
1. Usuario va a /manual (ManualIndex.tsx)
|
||
|
|
↓
|
||
|
|
2. Click en bloque → Expande (acordeón)
|
||
|
|
↓
|
||
|
|
3. Click en capítulo → Navigate a /manual/:parte/:bloque/:capitulo
|
||
|
|
↓
|
||
|
|
4. ManualViewer carga MarkdownViewer (línea 4)
|
||
|
|
↓
|
||
|
|
5. Navegación anterior/siguiente (línea 48-53)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### ⚠️ Puntos de Fricción
|
||
|
|
|
||
|
|
**1. Sin Breadcrumbs**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: ManualViewer.tsx no tiene breadcrumbs
|
||
|
|
// Usuario no sabe dónde está en la jerarquía
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir breadcrumbs
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Añadir breadcrumbs
|
||
|
|
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink } from '@/components/ui/breadcrumb';
|
||
|
|
|
||
|
|
<Breadcrumb>
|
||
|
|
<BreadcrumbList>
|
||
|
|
<BreadcrumbItem>
|
||
|
|
<BreadcrumbLink asChild>
|
||
|
|
<Link to="/manual">Manual</Link>
|
||
|
|
</BreadcrumbLink>
|
||
|
|
</BreadcrumbItem>
|
||
|
|
<BreadcrumbItem>
|
||
|
|
<BreadcrumbLink>{capituloData.bloque}</BreadcrumbLink>
|
||
|
|
</BreadcrumbItem>
|
||
|
|
<BreadcrumbItem>
|
||
|
|
<span>{capituloData.titulo}</span>
|
||
|
|
</BreadcrumbItem>
|
||
|
|
</BreadcrumbList>
|
||
|
|
</Breadcrumb>
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Navegación Anterior/Siguiente No Visible Hasta Scroll**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: ManualViewer.tsx (línea 55-126)
|
||
|
|
// Navegación anterior/siguiente solo visible al final del contenido
|
||
|
|
// En contenido largo, usuario no ve opciones de navegación hasta el final
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir sticky navigation
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Sticky navigation
|
||
|
|
<div className="sticky top-14 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b border-border py-3">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
{capituloAnterior && (
|
||
|
|
<Link to={capituloAnterior.ruta} className="flex items-center gap-2">
|
||
|
|
<ChevronLeft className="w-5 h-5" />
|
||
|
|
<span className="hidden sm:inline">Anterior</span>
|
||
|
|
</Link>
|
||
|
|
)}
|
||
|
|
{capituloSiguiente && (
|
||
|
|
<Link to={capituloSiguiente.ruta} className="flex items-center gap-2 ml-auto">
|
||
|
|
<span className="hidden sm:inline">Siguiente</span>
|
||
|
|
<ChevronRight className="w-5 h-5" />
|
||
|
|
</Link>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Sin Indicador de Progreso de Lectura**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: No hay indicador de cuánto ha leído el usuario
|
||
|
|
// No hay "X% completado" o barra de progreso
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir indicador de progreso
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.4 Estados de Carga/Error
|
||
|
|
|
||
|
|
#### ✅ Estados Implementados
|
||
|
|
|
||
|
|
| Componente | Loading | Error | Empty | Estado |
|
||
|
|
|------------|---------|-------|-------|--------|
|
||
|
|
| **MarkdownViewer** | ✅ Sí | ✅ Sí | ✅ Sí | ✅ Completo |
|
||
|
|
| **ManualViewer** | ⚠️ Parcial | ⚠️ Parcial | ✅ Sí | ⚠️ Mejorable |
|
||
|
|
| **SearchModal** | ❌ No | ❌ No | ✅ Sí | ❌ Falta |
|
||
|
|
| **ProcedureCard** | ❌ No | ❌ No | ❌ No | ❌ Falta |
|
||
|
|
|
||
|
|
#### ❌ Problemas Identificados
|
||
|
|
|
||
|
|
**1. MarkdownViewer Re-renderiza en Cada filePath Change**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: MarkdownViewer.tsx (línea 54-81)
|
||
|
|
useEffect(() => {
|
||
|
|
// Fetch se ejecuta cada vez que filePath cambia
|
||
|
|
// No hay cancelación de fetch anterior si cambia rápido
|
||
|
|
fetch(normalizedPath)
|
||
|
|
.then(...)
|
||
|
|
.catch(...);
|
||
|
|
}, [filePath, onError]);
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: Si filePath cambia rápidamente, múltiples fetches concurrentes
|
||
|
|
// ⚠️ PROBLEMA: Race condition - respuesta antigua puede sobrescribir nueva
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Cancelar fetch anterior
|
||
|
|
useEffect(() => {
|
||
|
|
const normalizedPath = filePath.startsWith('/') ? filePath : `/${filePath}`;
|
||
|
|
|
||
|
|
setLoading(true);
|
||
|
|
setError(null);
|
||
|
|
|
||
|
|
const abortController = new AbortController();
|
||
|
|
|
||
|
|
fetch(normalizedPath, { signal: abortController.signal })
|
||
|
|
.then((res) => {
|
||
|
|
if (!res.ok) {
|
||
|
|
throw new Error(`Error ${res.status}: ${res.statusText}`);
|
||
|
|
}
|
||
|
|
return res.text();
|
||
|
|
})
|
||
|
|
.then((text) => {
|
||
|
|
// Solo actualizar si el fetch no fue cancelado
|
||
|
|
if (!abortController.signal.aborted) {
|
||
|
|
setContent(text);
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.catch((err) => {
|
||
|
|
// Ignorar errores de cancelación
|
||
|
|
if (err.name === 'AbortError') return;
|
||
|
|
|
||
|
|
const errorMessage = `No se pudo cargar el archivo: ${err.message}`;
|
||
|
|
if (!abortController.signal.aborted) {
|
||
|
|
setError(errorMessage);
|
||
|
|
setLoading(false);
|
||
|
|
|
||
|
|
if (onError) {
|
||
|
|
onError(new Error(errorMessage));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
abortController.abort();
|
||
|
|
};
|
||
|
|
}, [filePath, onError]);
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Sin Skeleton Loading**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Loading states son spinners genéricos
|
||
|
|
// No hay skeleton loaders que muestren estructura del contenido esperado
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Añadir skeleton loaders
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ CREAR: src/components/shared/SkeletonLoader.tsx
|
||
|
|
export const MarkdownSkeleton = () => (
|
||
|
|
<div className="space-y-4 animate-pulse">
|
||
|
|
<div className="h-8 bg-muted rounded w-3/4" />
|
||
|
|
<div className="h-4 bg-muted rounded w-full" />
|
||
|
|
<div className="h-4 bg-muted rounded w-5/6" />
|
||
|
|
<div className="h-64 bg-muted rounded" /> {/* Imagen skeleton */}
|
||
|
|
<div className="h-4 bg-muted rounded w-full" />
|
||
|
|
<div className="h-4 bg-muted rounded w-4/5" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
// Usar en MarkdownViewer:
|
||
|
|
if (loading && showLoading) {
|
||
|
|
return <MarkdownSkeleton />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. ⚡ RENDIMIENTO FRONTEND
|
||
|
|
|
||
|
|
### 3.1 Bundle Analysis
|
||
|
|
|
||
|
|
#### ✅ Optimizaciones Implementadas
|
||
|
|
|
||
|
|
| Optimización | Estado | Detalles |
|
||
|
|
|--------------|--------|----------|
|
||
|
|
| **Lazy Loading de Páginas** | ✅ Implementado | `App.tsx` línea 23-54 |
|
||
|
|
| **Code Splitting** | ✅ Implementado | Vite config manual chunks |
|
||
|
|
| **Tree Shaking** | ✅ Automático | Vite + ES Modules |
|
||
|
|
| **Image Lazy Loading** | ✅ Implementado | `loading="lazy"` en imágenes |
|
||
|
|
|
||
|
|
#### ⚠️ Problemas Identificados
|
||
|
|
|
||
|
|
**1. Bundle Size Grande por react-markdown**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: react-markdown + plugins es pesado (~150KB gzipped)
|
||
|
|
// Se carga incluso si no se usa MarkdownViewer
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Lazy load react-markdown también
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Lazy load MarkdownViewer completo
|
||
|
|
const MarkdownViewer = lazy(() => import('@/components/content/MarkdownViewer'));
|
||
|
|
|
||
|
|
// En ManualViewer.tsx:
|
||
|
|
<Suspense fallback={<MarkdownSkeleton />}>
|
||
|
|
<MarkdownViewer filePath={filePath} />
|
||
|
|
</Suspense>
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Vendor Chunks Muy Grandes**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ⚠️ PROBLEMA: vite.config.ts (línea 59)
|
||
|
|
chunkSizeWarningLimit: 1000, // 1MB - muy alto
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: vendor-react puede ser > 500KB
|
||
|
|
// Debería optimizarse más
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Optimizar chunks más agresivamente
|
||
|
|
build: {
|
||
|
|
chunkSizeWarningLimit: 500, // Reducir a 500KB
|
||
|
|
rollupOptions: {
|
||
|
|
output: {
|
||
|
|
manualChunks: (id) => {
|
||
|
|
// Separar react-markdown en chunk propio
|
||
|
|
if (id.includes('react-markdown')) {
|
||
|
|
return 'vendor-markdown';
|
||
|
|
}
|
||
|
|
if (id.includes('remark') || id.includes('rehype')) {
|
||
|
|
return 'vendor-markdown-plugins';
|
||
|
|
}
|
||
|
|
// ... resto de lógica
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Imágenes Sin Optimización**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: Imágenes en public/assets/ no están optimizadas
|
||
|
|
// No hay WebP, no hay responsive images, no hay srcset
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Implementar optimización de imágenes
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Usar next/image o similar (o implementar manual)
|
||
|
|
// Crear componente OptimizedImage
|
||
|
|
|
||
|
|
interface OptimizedImageProps {
|
||
|
|
src: string;
|
||
|
|
alt: string;
|
||
|
|
width?: number;
|
||
|
|
height?: number;
|
||
|
|
sizes?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const OptimizedImage = ({ src, alt, width, height, sizes }: OptimizedImageProps) => {
|
||
|
|
// Detectar soporte WebP
|
||
|
|
const [supportsWebP, setSupportsWebP] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const webP = new Image();
|
||
|
|
webP.onload = webP.onerror = () => {
|
||
|
|
setSupportsWebP(webP.height === 2);
|
||
|
|
};
|
||
|
|
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const webPSrc = src.replace(/\.(jpg|jpeg|png)$/, '.webp');
|
||
|
|
|
||
|
|
return (
|
||
|
|
<picture>
|
||
|
|
{supportsWebP && <source srcSet={webPSrc} type="image/webp" />}
|
||
|
|
<img
|
||
|
|
src={src}
|
||
|
|
alt={alt}
|
||
|
|
width={width}
|
||
|
|
height={height}
|
||
|
|
sizes={sizes}
|
||
|
|
loading="lazy"
|
||
|
|
decoding="async"
|
||
|
|
/>
|
||
|
|
</picture>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Re-renders Innecesarios
|
||
|
|
|
||
|
|
#### ❌ Problemas Identificados
|
||
|
|
|
||
|
|
**1. ProcedureCard Re-renderiza en Cada Toggle**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: ProcedureCard.tsx (línea 22-33)
|
||
|
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||
|
|
const { isFavorite, toggleFavorite: toggleFavoriteHook } = useFavorites();
|
||
|
|
|
||
|
|
// ⚠️ PROBLEMA: useFavorites() puede causar re-renders en todos los cards
|
||
|
|
// cuando cambia un favorito
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Memoizar componente
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido:**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Memoizar ProcedureCard
|
||
|
|
import { memo, useCallback } from 'react';
|
||
|
|
|
||
|
|
const ProcedureCard = memo(({ procedure, defaultExpanded = false }: ProcedureCardProps) => {
|
||
|
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||
|
|
const { isFavorite, toggleFavorite } = useFavorites();
|
||
|
|
|
||
|
|
const handleToggleFavorite = useCallback((e: React.MouseEvent) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
toggleFavorite({
|
||
|
|
id: procedure.id,
|
||
|
|
type: 'procedure',
|
||
|
|
title: procedure.shortTitle,
|
||
|
|
path: `/soporte-vital?id=${procedure.id}`,
|
||
|
|
});
|
||
|
|
}, [procedure.id, procedure.shortTitle, toggleFavorite]);
|
||
|
|
|
||
|
|
// ... resto del componente
|
||
|
|
}, (prevProps, nextProps) => {
|
||
|
|
// Comparación personalizada para evitar re-renders innecesarios
|
||
|
|
return (
|
||
|
|
prevProps.procedure.id === nextProps.procedure.id &&
|
||
|
|
prevProps.defaultExpanded === nextProps.defaultExpanded
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
ProcedureCard.displayName = 'ProcedureCard';
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. SearchModal Re-renderiza en Cada Query Change**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: SearchModal.tsx (línea 54-118)
|
||
|
|
// useEffect ejecuta búsqueda en cada cambio de query
|
||
|
|
// Sin debounce, sin memoización de resultados
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Debounce + memoización
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fix sugerido (ver sección 2.1 para debounce):**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ✅ FIX: Memoizar resultados de búsqueda
|
||
|
|
import { useMemo } from 'react';
|
||
|
|
|
||
|
|
const searchResults = useMemo(() => {
|
||
|
|
if (debouncedQuery.length < 2) return [];
|
||
|
|
|
||
|
|
const expandedQuery = expandQueryWithAcronyms(debouncedQuery);
|
||
|
|
// ... lógica de búsqueda
|
||
|
|
return [...procedures, ...drugs];
|
||
|
|
}, [debouncedQuery, typeFilter, categoryFilter]);
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. MarkdownViewer Re-renderiza Props Sin Cambios**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ PROBLEMA: MarkdownViewer.tsx
|
||
|
|
// No está memoizado, re-renderiza incluso si filePath no cambia
|
||
|
|
|
||
|
|
// ✅ SOLUCIÓN: Memoizar
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 Optimización de Assets
|
||
|
|
|
||
|
|
**Problemas:**
|
||
|
|
|
||
|
|
1. ❌ Sin compresión de imágenes (WebP, AVIF)
|
||
|
|
2. ❌ Sin responsive images (srcset)
|
||
|
|
3. ❌ Sin preload de assets críticos
|
||
|
|
4. ⚠️ Sin service worker para cache de assets
|
||
|
|
|
||
|
|
**Soluciones:**
|
||
|
|
|
||
|
|
1. ✅ Implementar optimización de imágenes (ver 3.1)
|
||
|
|
2. ✅ Añadir preload de assets críticos en `index.html`
|
||
|
|
3. ✅ Mejorar service worker para cache agresivo de assets
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 🔧 MEJORAS CONCRETAS - REFACTORS ESPECÍFICOS
|
||
|
|
|
||
|
|
### 4.1 Refactor de Componentes
|
||
|
|
|
||
|
|
#### Prioridad Alta
|
||
|
|
|
||
|
|
| Componente | Problema | Fix | Esfuerzo | Impacto |
|
||
|
|
|------------|----------|-----|----------|---------|
|
||
|
|
| **BaseCard** | Duplicación lógica | Crear componente base | 4 horas | Alto |
|
||
|
|
| **LoadingState** | Estados inconsistentes | Unificar componente | 2 horas | Medio |
|
||
|
|
| **ErrorState** | Estados inconsistentes | Unificar componente | 2 horas | Medio |
|
||
|
|
| **OptimizedImage** | Sin optimización | Implementar componente | 4 horas | Alto |
|
||
|
|
| **useDebounce** | Falta debounce | Crear hook | 1 hora | Medio |
|
||
|
|
|
||
|
|
#### Prioridad Media
|
||
|
|
|
||
|
|
| Componente | Problema | Fix | Esfuerzo | Impacto |
|
||
|
|
|------------|----------|-----|----------|---------|
|
||
|
|
| **OnboardingFlow** | No existe onboarding | Crear componente | 1 día | Alto |
|
||
|
|
| **Breadcrumbs** | Falta navegación clara | Añadir a ManualViewer | 2 horas | Medio |
|
||
|
|
| **SkeletonLoader** | Loading genérico | Crear skeletons específicos | 3 horas | Medio |
|
||
|
|
| **StickyNavigation** | Navegación no visible | Implementar sticky nav | 2 horas | Medio |
|
||
|
|
|
||
|
|
### 4.2 Librerías Recomendadas
|
||
|
|
|
||
|
|
| Problema | Librería | Razón | Instalación |
|
||
|
|
|----------|----------|-------|-------------|
|
||
|
|
| **Debounce** | `use-debounce` | Hook ya implementado | `npm i use-debounce` |
|
||
|
|
| **Accesibilidad** | `@react-aria/*` | Componentes accesibles | `npm i @react-aria/button` |
|
||
|
|
| **Imágenes** | `next/image` (adaptar) | Optimización automática | O implementar manual |
|
||
|
|
| **Análisis** | `web-vitals` | Métricas de rendimiento | `npm i web-vitals` |
|
||
|
|
| **Testing A11y** | `@axe-core/react` | Testing accesibilidad | `npm i @axe-core/react` |
|
||
|
|
|
||
|
|
### 4.3 Archivos que Necesitan Atención Urgente
|
||
|
|
|
||
|
|
#### 🔴 Crítico (1-2 días)
|
||
|
|
|
||
|
|
1. **`src/components/content/MarkdownViewer.tsx`**
|
||
|
|
- ✅ Añadir cancelación de fetch (race condition)
|
||
|
|
- ✅ Memoizar componente
|
||
|
|
- ✅ Mejorar manejo de errores
|
||
|
|
|
||
|
|
2. **`src/hooks/use-mobile.tsx`**
|
||
|
|
- ✅ Fix hydration mismatch
|
||
|
|
- ✅ Usar MediaQueryList API
|
||
|
|
|
||
|
|
3. **`src/components/layout/SearchModal.tsx`**
|
||
|
|
- ✅ Añadir debounce
|
||
|
|
- ✅ Memoizar resultados
|
||
|
|
- ✅ Añadir loading state
|
||
|
|
|
||
|
|
#### 🟡 Importante (3-5 días)
|
||
|
|
|
||
|
|
4. **`src/components/procedures/ProcedureCard.tsx`**
|
||
|
|
- ✅ Memoizar componente
|
||
|
|
- ✅ Extraer a BaseCard
|
||
|
|
- ✅ Optimizar re-renders
|
||
|
|
|
||
|
|
5. **`src/pages/Index.tsx`**
|
||
|
|
- ✅ Añadir aria-labels
|
||
|
|
- ✅ Fix responsive grid
|
||
|
|
- ✅ Fix floating button safe area
|
||
|
|
|
||
|
|
6. **`src/pages/ManualViewer.tsx`**
|
||
|
|
- ✅ Añadir breadcrumbs
|
||
|
|
- ✅ Sticky navigation
|
||
|
|
- ✅ Indicador de progreso
|
||
|
|
|
||
|
|
#### 🟢 Mejoras (1-2 semanas)
|
||
|
|
|
||
|
|
7. Crear componentes compartidos (BaseCard, LoadingState, ErrorState)
|
||
|
|
8. Implementar onboarding flow
|
||
|
|
9. Optimización de imágenes completa
|
||
|
|
10. Añadir skeleton loaders
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 📊 TABLA RESUMEN DE PROBLEMAS
|
||
|
|
|
||
|
|
| ID | Problema | Severidad | Archivo | Línea | Esfuerzo Fix | Prioridad |
|
||
|
|
|----|----------|-----------|---------|-------|--------------|-----------|
|
||
|
|
| **U1** | Falta aria-labels en botones | 🔴 Alta | `Index.tsx` | 37 | 30 min | 1º |
|
||
|
|
| **U2** | Race condition en MarkdownViewer | 🔴 Alta | `MarkdownViewer.tsx` | 54-81 | 1 hora | 2º |
|
||
|
|
| **U3** | Hydration mismatch en useIsMobile | 🔴 Alta | `use-mobile.tsx` | 6-18 | 1 hora | 3º |
|
||
|
|
| **U4** | Sin debounce en búsqueda | 🟡 Media | `SearchModal.tsx` | 54-58 | 1 hora | 4º |
|
||
|
|
| **U5** | Duplicación lógica en Cards | 🟡 Media | `ProcedureCard.tsx` | Todo | 4 horas | 5º |
|
||
|
|
| **U6** | Re-renders innecesarios | 🟡 Media | `ProcedureCard.tsx` | 22 | 2 horas | 6º |
|
||
|
|
| **U7** | Sin skeleton loaders | 🟡 Media | Múltiples | - | 3 horas | 7º |
|
||
|
|
| **U8** | Sin breadcrumbs | 🟡 Media | `ManualViewer.tsx` | - | 2 horas | 8º |
|
||
|
|
| **U9** | Imágenes sin optimizar | 🟡 Media | `MarkdownViewer.tsx` | 278-354 | 4 horas | 9º |
|
||
|
|
| **U10** | Sin focus visible | 🟡 Media | Múltiples | - | 2 horas | 10º |
|
||
|
|
| **U11** | Sin skip links | 🟢 Baja | `App.tsx` | - | 30 min | 11º |
|
||
|
|
| **U12** | Sin onboarding | 🟢 Baja | Nuevo | - | 1 día | 12º |
|
||
|
|
|
||
|
|
**Total Problemas:** 12
|
||
|
|
**Críticos:** 3
|
||
|
|
**Importantes:** 7
|
||
|
|
**Mejoras:** 2
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. ✅ CHECKLIST DE IMPLEMENTACIÓN
|
||
|
|
|
||
|
|
### Quick Wins (1 día)
|
||
|
|
|
||
|
|
- [ ] U1: Añadir aria-labels a botones principales
|
||
|
|
- [ ] U2: Fix race condition en MarkdownViewer
|
||
|
|
- [ ] U3: Fix hydration mismatch en useIsMobile
|
||
|
|
- [ ] U4: Añadir debounce en búsqueda
|
||
|
|
- [ ] U11: Añadir skip links
|
||
|
|
|
||
|
|
### Refactors Importantes (1 semana)
|
||
|
|
|
||
|
|
- [ ] U5: Crear BaseCard component
|
||
|
|
- [ ] U6: Memoizar ProcedureCard
|
||
|
|
- [ ] U7: Implementar skeleton loaders
|
||
|
|
- [ ] U8: Añadir breadcrumbs
|
||
|
|
- [ ] U10: Añadir focus visible styles
|
||
|
|
|
||
|
|
### Optimizaciones (1-2 semanas)
|
||
|
|
|
||
|
|
- [ ] U9: Optimizar imágenes (WebP, srcset)
|
||
|
|
- [ ] U12: Implementar onboarding flow
|
||
|
|
- [ ] Mejorar bundle splitting
|
||
|
|
- [ ] Añadir web-vitals tracking
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Última actualización:** 2025-01-07
|
||
|
|
**Próxima revisión recomendada:** 2025-02-07 (1 mes)
|
||
|
|
|