From 1783db4b2ce9168935c4072ebdb0588ad9eff3f7 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 6 Sep 2025 17:56:35 -0600 Subject: [PATCH] UX mejorada --- backend/server-prod.js | 51 +++++++++++++++++++++++++++++++++++++++++- backend/server.js | 46 ++++++++++++++++++++++++++++++++++++- src/App.jsx | 44 ++++++++++++++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/backend/server-prod.js b/backend/server-prod.js index 0e6b46f..f6adb40 100644 --- a/backend/server-prod.js +++ b/backend/server-prod.js @@ -20,6 +20,29 @@ const client = new MongoClient(uri); let db; let amigosCollection; +const clients = new Set(); + +function sseHeaders(res) { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders && res.flushHeaders(); +} + +function sendEvent(res, event, data) { + if (event) res.write(`event: ${event}\n`); + res.write(`data: ${JSON.stringify(data)}\n\n`); +} + +function broadcast(event, data) { + for (const res of clients) { + try { + sendEvent(res, event, data); + } catch (_) { + // ignore write errors; client will be removed on close + } + } +} // Connect to MongoDB async function connectDB() { @@ -67,11 +90,37 @@ app.post('/api/amigos', async (req, res) => { id: result.insertedId, nombre }); + + // Notify SSE clients about the new friend + broadcast('amigoAdded', { id: result.insertedId, nombre }); } catch (error) { res.status(500).json({ error: error.message }); } }); +// Server-Sent Events for realtime updates +app.get('/api/events', async (req, res) => { + sseHeaders(res); + clients.add(res); + + // Send initial snapshot + try { + if (amigosCollection) { + const amigos = await amigosCollection.find({}).toArray(); + sendEvent(res, 'init', { amigos }); + } else { + sendEvent(res, 'init', { amigos: [] }); + } + } catch (_) { + sendEvent(res, 'init', { amigos: [] }); + } + + req.on('close', () => { + clients.delete(res); + try { res.end(); } catch (_) {} + }); +}); + // Health check endpoint app.get('/health', (req, res) => { res.json({ @@ -91,4 +140,4 @@ connectDB().then(() => { console.log(`Server running on http://0.0.0.0:${PORT}`); console.log(`MongoDB host: ${mongoHost}`); }); -}); \ No newline at end of file +}); diff --git a/backend/server.js b/backend/server.js index 1371cd9..3b74067 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,6 +15,29 @@ const client = new MongoClient(uri); let db; let amigosCollection; +const clients = new Set(); + +function sseHeaders(res) { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders && res.flushHeaders(); +} + +function sendEvent(res, event, data) { + if (event) res.write(`event: ${event}\n`); + res.write(`data: ${JSON.stringify(data)}\n\n`); +} + +function broadcast(event, data) { + for (const res of clients) { + try { + sendEvent(res, event, data); + } catch (_) { + // client may be closed + } + } +} // Connect to MongoDB async function connectDB() { @@ -62,14 +85,35 @@ app.post('/api/amigos', async (req, res) => { id: result.insertedId, nombre }); + + // SSE notify + broadcast('amigoAdded', { id: result.insertedId, nombre }); } catch (error) { res.status(500).json({ error: error.message }); } }); +// SSE endpoint +app.get('/api/events', async (req, res) => { + sseHeaders(res); + clients.add(res); + + try { + const amigos = await amigosCollection.find({}).toArray(); + sendEvent(res, 'init', { amigos }); + } catch (_) { + sendEvent(res, 'init', { amigos: [] }); + } + + req.on('close', () => { + clients.delete(res); + try { res.end(); } catch (_) {} + }); +}); + // Start server connectDB().then(() => { app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); -}); \ No newline at end of file +}); diff --git a/src/App.jsx b/src/App.jsx index 9e3f083..0f8a764 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useEffect, useRef, useState } from 'react' import './App.css' // Configure API base URL via Vite env, default to same-origin @@ -9,11 +9,47 @@ function App() { const [amigos, setAmigos] = useState([]) const [loading, setLoading] = useState(false) const [message, setMessage] = useState('') + const inputRef = useRef(null) useEffect(() => { fetchAmigos() }, []) + useEffect(() => { + // Subscribe to SSE for realtime updates + const url = `${API_BASE}/api/events` + const es = new EventSource(url) + + const onInit = (e) => { + try { + const payload = JSON.parse(e.data) + if (Array.isArray(payload.amigos)) setAmigos(payload.amigos) + } catch (_) {} + } + + const onAdded = (e) => { + try { + const payload = JSON.parse(e.data) + setAmigos((prev) => [ + ...prev, + { _id: payload.id || payload._id, nombre: payload.nombre } + ]) + } catch (_) {} + } + + es.addEventListener('init', onInit) + es.addEventListener('amigoAdded', onAdded) + es.onerror = () => { + // Let browser auto-reconnect; optionally show a message + } + + return () => { + es.removeEventListener('init', onInit) + es.removeEventListener('amigoAdded', onAdded) + es.close() + } + }, []) + const fetchAmigos = async () => { try { const response = await fetch(`${API_BASE}/api/amigos`) @@ -49,7 +85,7 @@ function App() { if (response.ok) { setMessage(`ยก${data.nombre} agregado exitosamente!`) setNombre('') - fetchAmigos() + // No refetch here; SSE will push the update } else { setMessage(data.error || 'Error al agregar amigo') } @@ -58,6 +94,8 @@ function App() { console.error('Error:', error) } finally { setLoading(false) + // Keep focus on the input for rapid entry + inputRef.current?.focus() } } @@ -74,6 +112,8 @@ function App() { value={nombre} onChange={(e) => setNombre(e.target.value)} placeholder="Ingresa el nombre" + ref={inputRef} + autoFocus disabled={loading} />