UX mejorada
This commit is contained in:
@@ -20,6 +20,29 @@ const client = new MongoClient(uri);
|
|||||||
|
|
||||||
let db;
|
let db;
|
||||||
let amigosCollection;
|
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
|
// Connect to MongoDB
|
||||||
async function connectDB() {
|
async function connectDB() {
|
||||||
@@ -67,11 +90,37 @@ app.post('/api/amigos', async (req, res) => {
|
|||||||
id: result.insertedId,
|
id: result.insertedId,
|
||||||
nombre
|
nombre
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify SSE clients about the new friend
|
||||||
|
broadcast('amigoAdded', { id: result.insertedId, nombre });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
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
|
// Health check endpoint
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
@@ -91,4 +140,4 @@ connectDB().then(() => {
|
|||||||
console.log(`Server running on http://0.0.0.0:${PORT}`);
|
console.log(`Server running on http://0.0.0.0:${PORT}`);
|
||||||
console.log(`MongoDB host: ${mongoHost}`);
|
console.log(`MongoDB host: ${mongoHost}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,29 @@ const client = new MongoClient(uri);
|
|||||||
|
|
||||||
let db;
|
let db;
|
||||||
let amigosCollection;
|
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
|
// Connect to MongoDB
|
||||||
async function connectDB() {
|
async function connectDB() {
|
||||||
@@ -62,14 +85,35 @@ app.post('/api/amigos', async (req, res) => {
|
|||||||
id: result.insertedId,
|
id: result.insertedId,
|
||||||
nombre
|
nombre
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SSE notify
|
||||||
|
broadcast('amigoAdded', { id: result.insertedId, nombre });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
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
|
// Start server
|
||||||
connectDB().then(() => {
|
connectDB().then(() => {
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on http://localhost:${PORT}`);
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
44
src/App.jsx
44
src/App.jsx
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
// Configure API base URL via Vite env, default to same-origin
|
// Configure API base URL via Vite env, default to same-origin
|
||||||
@@ -9,11 +9,47 @@ function App() {
|
|||||||
const [amigos, setAmigos] = useState([])
|
const [amigos, setAmigos] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
|
const inputRef = useRef(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAmigos()
|
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 () => {
|
const fetchAmigos = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/amigos`)
|
const response = await fetch(`${API_BASE}/api/amigos`)
|
||||||
@@ -49,7 +85,7 @@ function App() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMessage(`¡${data.nombre} agregado exitosamente!`)
|
setMessage(`¡${data.nombre} agregado exitosamente!`)
|
||||||
setNombre('')
|
setNombre('')
|
||||||
fetchAmigos()
|
// No refetch here; SSE will push the update
|
||||||
} else {
|
} else {
|
||||||
setMessage(data.error || 'Error al agregar amigo')
|
setMessage(data.error || 'Error al agregar amigo')
|
||||||
}
|
}
|
||||||
@@ -58,6 +94,8 @@ function App() {
|
|||||||
console.error('Error:', error)
|
console.error('Error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
// Keep focus on the input for rapid entry
|
||||||
|
inputRef.current?.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +112,8 @@ function App() {
|
|||||||
value={nombre}
|
value={nombre}
|
||||||
onChange={(e) => setNombre(e.target.value)}
|
onChange={(e) => setNombre(e.target.value)}
|
||||||
placeholder="Ingresa el nombre"
|
placeholder="Ingresa el nombre"
|
||||||
|
ref={inputRef}
|
||||||
|
autoFocus
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user