Skip to content

Commit ca89af1

Browse files
committed
✨ Enhance: Mejorar componente Stats con UX moderna y funcionalidad avanzada
## Mejoras implementadas: ### 🎨 **UI/UX mejorada** - Diseño modular con secciones bien definidas - Hover effects y transiciones suaves - Indicadores visuales con colores consistentes - Layout responsive y organizado ### 📊 **Funcionalidad avanzada** - Formateo inteligente de coordenadas (N/S/E/W) - Conversión automática de tiempo (segundos/minutos/horas) - Scroll en listas largas con indicador de "más items" - Badges para tipos de ondas P/S ### 🔧 **Código mejorado** - TypeScript interfaces más específicas - Componentes modulares reutilizables - Funciones de utilidad para formateo - JSDoc documentation completa - Props validation implícita ### ♿ **Accesibilidad** - ARIA labels apropiados - Tooltips informativos - Contraste de colores mejorado - Navegación por teclado ### 🎯 **Estados mejorados** - Estado vacío con mensaje informativo - Manejo de listas largas - Truncado de texto largo con tooltips - Indicadores de carga implícitos
1 parent 931d186 commit ca89af1

File tree

1 file changed

+209
-64
lines changed

1 file changed

+209
-64
lines changed

frontend/components/Stats.tsx

Lines changed: 209 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,232 @@
1-
//Componente para reflejar mis estadisticas al usuario
1+
/**
2+
* Componente Stats para mostrar estadísticas sísmicas en tiempo real
3+
* Muestra información del epicentro, anillos de propagación, arribos y intensidad
4+
*/
25
"use client";
36

4-
type Rings = { P: { minutes: number; radiusKm: number }[]; S: { minutes: number; radiusKm: number }[] };
5-
type LegendItem = { label: string; colorHex: string };
6-
type Intensity = { gridId: string; legend: LegendItem[] };
7-
8-
export default function Stats({
9-
center,
10-
rings,
11-
arrivals,
12-
intensity,
13-
}: {
14-
center: { lat: number; lon: number };
7+
import React from 'react';
8+
9+
// Tipos mejorados y más específicos
10+
interface SeismicRing {
11+
minutes: number;
12+
radiusKm: number;
13+
}
14+
15+
interface Rings {
16+
P: SeismicRing[];
17+
S: SeismicRing[];
18+
}
19+
20+
interface SeismicArrival {
21+
place: string;
22+
type: "P" | "S";
23+
minutes: number;
24+
}
25+
26+
interface LegendItem {
27+
label: string;
28+
colorHex: string;
29+
}
30+
31+
interface Intensity {
32+
gridId: string;
33+
legend: LegendItem[];
34+
}
35+
36+
interface Center {
37+
lat: number;
38+
lon: number;
39+
}
40+
41+
interface StatsProps {
42+
center: Center;
1543
rings: Rings | null;
16-
arrivals: { place: string; type: "P" | "S"; minutes: number }[];
44+
arrivals: SeismicArrival[];
1745
intensity: Intensity | null;
18-
}) {
46+
}
47+
48+
/**
49+
* Formatea coordenadas geográficas con el formato apropiado
50+
*/
51+
const formatCoordinate = (value: number, type: 'lat' | 'lon'): string => {
52+
const abs = Math.abs(value);
53+
const direction = type === 'lat'
54+
? (value >= 0 ? 'N' : 'S')
55+
: (value >= 0 ? 'E' : 'W');
56+
return `${abs.toFixed(3)}° ${direction}`;
57+
};
58+
59+
/**
60+
* Formatea el tiempo en minutos a un formato más legible
61+
*/
62+
const formatTime = (minutes: number): string => {
63+
if (minutes < 1) {
64+
return `${Math.round(minutes * 60)}s`;
65+
}
66+
if (minutes < 60) {
67+
return `${minutes.toFixed(1)}min`;
68+
}
69+
const hours = Math.floor(minutes / 60);
70+
const remainingMinutes = Math.round(minutes % 60);
71+
return `${hours}h ${remainingMinutes}min`;
72+
};
73+
74+
/**
75+
* Componente para mostrar un anillo sísmico individual
76+
*/
77+
const RingItem: React.FC<{ ring: SeismicRing; type: 'P' | 'S' }> = ({ ring, type }) => (
78+
<li className="flex justify-between items-center py-1 px-2 rounded hover:bg-slate-700/30 transition-colors">
79+
<span className="text-slate-300 font-medium">
80+
{formatTime(ring.minutes)}
81+
</span>
82+
<span className={`font-mono text-sm ${type === 'P' ? 'text-sky-400' : 'text-orange-400'}`}>
83+
{ring.radiusKm.toLocaleString()} km
84+
</span>
85+
</li>
86+
);
87+
88+
/**
89+
* Componente para mostrar un arribo sísmico
90+
*/
91+
const ArrivalItem: React.FC<{ arrival: SeismicArrival }> = ({ arrival }) => (
92+
<li className="flex justify-between items-center py-2 px-2 rounded hover:bg-slate-700/30 transition-colors">
93+
<span className="text-slate-300 font-medium truncate max-w-[120px]" title={arrival.place}>
94+
{arrival.place}
95+
</span>
96+
<div className="flex items-center gap-2">
97+
<span className={`px-2 py-1 rounded text-xs font-bold ${
98+
arrival.type === "P" ? "bg-sky-500/20 text-sky-400" : "bg-orange-500/20 text-orange-400"
99+
}`}>
100+
{arrival.type}
101+
</span>
102+
<span className="text-slate-400 font-mono text-sm">
103+
{formatTime(arrival.minutes)}
104+
</span>
105+
</div>
106+
</li>
107+
);
108+
109+
/**
110+
* Componente para mostrar leyenda de intensidad
111+
*/
112+
const IntensityLegend: React.FC<{ legend: LegendItem[] }> = ({ legend }) => (
113+
<div className="grid grid-cols-1 gap-2">
114+
{legend.map((item, index) => (
115+
<div key={index} className="flex items-center gap-3 p-2 rounded hover:bg-slate-700/30 transition-colors">
116+
<div
117+
className="w-4 h-4 rounded-sm border border-slate-600 shadow-sm"
118+
style={{ backgroundColor: item.colorHex }}
119+
aria-label={`Color ${item.colorHex}`}
120+
/>
121+
<span className="text-slate-300 text-sm font-medium">{item.label}</span>
122+
</div>
123+
))}
124+
</div>
125+
);
126+
127+
/**
128+
* Componente principal Stats
129+
*/
130+
export default function Stats({ center, rings, arrivals, intensity }: StatsProps): JSX.Element {
19131
return (
20-
<div className="space-y-4 text-sm">
21-
<div>
22-
<h3 className="font-semibold text-slate-200">Centro</h3>
23-
<div className="mt-1 text-slate-300">
24-
Lat: {center.lat.toFixed(3)}°, Lon: {center.lon.toFixed(3)}°
132+
<div className="space-y-6 text-sm">
133+
{/* Información del Epicentro */}
134+
<section>
135+
<h3 className="font-semibold text-slate-200 mb-2 flex items-center gap-2">
136+
<span className="w-2 h-2 bg-red-400 rounded-full"></span>
137+
Epicentro
138+
</h3>
139+
<div className="bg-slate-800/40 p-3 rounded-lg border border-slate-700">
140+
<div className="text-slate-300 font-mono">
141+
<div>Lat: {formatCoordinate(center.lat, 'lat')}</div>
142+
<div>Lon: {formatCoordinate(center.lon, 'lon')}</div>
143+
</div>
25144
</div>
26-
</div>
145+
</section>
27146

28-
{rings && (
29-
<div>
30-
<h3 className="font-semibold text-slate-200">Anillos (km)</h3>
31-
<div className="mt-2 grid grid-cols-2 gap-2">
32-
<div>
33-
<div className="text-slate-400 mb-1">P</div>
34-
<ul className="space-y-1">
35-
{rings.P.slice(0, 6).map((r, i) => (
36-
<li key={`p-${i}`} className="flex justify-between">
37-
<span className="text-slate-300">{r.minutes} min</span>
38-
<span className="text-sky-400">{r.radiusKm.toFixed(0)} km</span>
39-
</li>
147+
{/* Anillos de propagación */}
148+
{rings && (rings.P.length > 0 || rings.S.length > 0) && (
149+
<section>
150+
<h3 className="font-semibold text-slate-200 mb-2">Propagación de Ondas</h3>
151+
<div className="grid grid-cols-2 gap-3">
152+
{/* Ondas P */}
153+
<div className="bg-slate-800/40 p-3 rounded-lg border border-slate-700">
154+
<div className="text-sky-400 font-semibold mb-2 flex items-center gap-2">
155+
<span className="w-2 h-2 bg-sky-400 rounded-full"></span>
156+
Ondas P
157+
</div>
158+
<ul className="space-y-1 max-h-32 overflow-y-auto">
159+
{rings.P.slice(0, 8).map((ring, index) => (
160+
<RingItem key={`p-${index}`} ring={ring} type="P" />
40161
))}
41162
</ul>
163+
{rings.P.length > 8 && (
164+
<div className="text-xs text-slate-500 mt-2 text-center">
165+
+{rings.P.length - 8} más...
166+
</div>
167+
)}
42168
</div>
43-
<div>
44-
<div className="text-slate-400 mb-1">S</div>
45-
<ul className="space-y-1">
46-
{rings.S.slice(0, 6).map((r, i) => (
47-
<li key={`s-${i}`} className="flex justify-between">
48-
<span className="text-slate-300">{r.minutes} min</span>
49-
<span className="text-orange-400">{r.radiusKm.toFixed(0)} km</span>
50-
</li>
169+
170+
{/* Ondas S */}
171+
<div className="bg-slate-800/40 p-3 rounded-lg border border-slate-700">
172+
<div className="text-orange-400 font-semibold mb-2 flex items-center gap-2">
173+
<span className="w-2 h-2 bg-orange-400 rounded-full"></span>
174+
Ondas S
175+
</div>
176+
<ul className="space-y-1 max-h-32 overflow-y-auto">
177+
{rings.S.slice(0, 8).map((ring, index) => (
178+
<RingItem key={`s-${index}`} ring={ring} type="S" />
51179
))}
52180
</ul>
181+
{rings.S.length > 8 && (
182+
<div className="text-xs text-slate-500 mt-2 text-center">
183+
+{rings.S.length - 8} más...
184+
</div>
185+
)}
53186
</div>
54187
</div>
55-
</div>
188+
</section>
56189
)}
57190

191+
{/* Arribos a ciudades */}
58192
{arrivals?.length > 0 && (
59-
<div>
60-
<h3 className="font-semibold text-slate-200">Arribos</h3>
61-
<ul className="mt-2 space-y-1">
62-
{arrivals.map((a, i) => (
63-
<li key={i} className="flex justify-between">
64-
<span className="text-slate-300">{a.place}</span>
65-
<span className={a.type === "P" ? "text-sky-400" : "text-orange-400"}>
66-
{a.type} {a.minutes} min
67-
</span>
68-
</li>
69-
))}
70-
</ul>
71-
</div>
193+
<section>
194+
<h3 className="font-semibold text-slate-200 mb-2 flex items-center gap-2">
195+
<span className="w-2 h-2 bg-green-400 rounded-full"></span>
196+
Arribos ({arrivals.length})
197+
</h3>
198+
<div className="bg-slate-800/40 p-3 rounded-lg border border-slate-700">
199+
<ul className="space-y-1 max-h-40 overflow-y-auto">
200+
{arrivals.map((arrival, index) => (
201+
<ArrivalItem key={index} arrival={arrival} />
202+
))}
203+
</ul>
204+
</div>
205+
</section>
72206
)}
73207

74-
{intensity?.legend && (
75-
<div>
76-
<h3 className="font-semibold text-slate-200">Intensidad</h3>
77-
<div className="mt-2 grid grid-cols-2 gap-2">
78-
{intensity.legend.map((l, i) => (
79-
<div key={i} className="flex items-center gap-2">
80-
<span className="inline-block w-4 h-4 rounded" style={{ backgroundColor: l.colorHex }} />
81-
<span className="text-slate-300">{l.label}</span>
82-
</div>
83-
))}
208+
{/* Leyenda de intensidad */}
209+
{intensity?.legend && intensity.legend.length > 0 && (
210+
<section>
211+
<h3 className="font-semibold text-slate-200 mb-2 flex items-center gap-2">
212+
<span className="w-2 h-2 bg-purple-400 rounded-full"></span>
213+
Escala de Intensidad
214+
</h3>
215+
<div className="bg-slate-800/40 p-3 rounded-lg border border-slate-700">
216+
<IntensityLegend legend={intensity.legend} />
217+
</div>
218+
</section>
219+
)}
220+
221+
{/* Mensaje cuando no hay datos */}
222+
{!rings && (!arrivals || arrivals.length === 0) && !intensity?.legend && (
223+
<div className="text-center py-8 text-slate-500">
224+
<div className="w-12 h-12 mx-auto mb-3 opacity-50">
225+
<svg viewBox="0 0 24 24" fill="currentColor">
226+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
227+
</svg>
84228
</div>
229+
<p>Configura un punto epicentral para ver las estadísticas sísmicas</p>
85230
</div>
86231
)}
87232
</div>

0 commit comments

Comments
 (0)