/* news.jsx — agenda manuale di eventi/catalizzatori datati. Eventi inseriti a mano (lanci, prodotti, IPO...) raggruppati per giorno. Niente LLM: è la TUA agenda. CRUD via /api/news-events. */ (function () { const { useState, useEffect } = React; const _CAT_EMOJI = { lancio: "🚀", prodotto: "📱", ipo: "🆕", macro: "🏛️", earnings: "📊", regolatorio: "⚖️", roadmap: "🗺️", evento: "📅", altro: "•", }; const _MESI = ["gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"]; const _GIORNI = ["dom", "lun", "mar", "mer", "gio", "ven", "sab"]; function _fmtDay(iso) { const d = new Date(iso + "T12:00:00"); return `${_GIORNI[d.getDay()]} ${d.getDate()} ${_MESI[d.getMonth()]}`; } function _daysAway(iso) { const today = new Date(); today.setHours(0, 0, 0, 0); const d = new Date(iso + "T00:00:00"); return Math.round((d - today) / 86400000); } function _fmtArtDate(iso) { if (!iso) return ""; const d = new Date(iso + "T12:00:00"); if (isNaN(d)) return ""; return `${d.getDate()} ${_MESI[d.getMonth()]}`; } // Lista IPO/Spin-off (spostata da Top Picks) — con data articolo + publisher function DiscoveryList({ events }) { if (events === null) return
Caricamento notizie…
; const ipo = events.filter(e => e.type === "ipo"); const spinoff = events.filter(e => e.type === "spinoff"); const Group = ({ label, color, list }) => list.length === 0 ? null : (
{label} ({list.length})
{list.map((e, i) => (
{e.title}
{e.published && 🗓 {_fmtArtDate(e.published)}} {e.publisher && {e.publisher}} {e.url && apri ↗}
))}
); return (
{events.length === 0 &&
Nessuna notizia IPO/Spin-off al momento.
}
Notizie raccolte da Tool5 (ricerca news, senza ticker). La data è quella dell'articolo; le voci si rinnovano ogni giorno. Per eventi tuoi con data certa usa l'Agenda.
); } const _EMPTY_FORM = { event_date: "", event_end_date: "", title: "", symbol: "", category: "lancio", note: "" }; window.NewsView = function NewsView() { const [events, setEvents] = useState([]); const [cats, setCats] = useState([]); const [showPast, setShowPast] = useState(false); const [showForm, setShowForm] = useState(false); const [editId, setEditId] = useState(null); // id evento in modifica | null const [confirmDel, setConfirmDel] = useState(null); // evento da eliminare | null const [mode, setMode] = useState("agenda"); // "agenda" | "discovery" const [discovery, setDiscovery] = useState(null); // IPO/Spin-off da Tool5 const [err, setErr] = useState(null); const [form, setForm] = useState(_EMPTY_FORM); const [saving, setSaving] = useState(false); const load = () => { fetch("/api/news-events").then(r => r.json()).then(d => { if (d.error) setErr(d.error); else { setEvents(d.events || []); setCats(d.categories || []); setErr(null); } }).catch(e => setErr(String(e))); }; useEffect(load, []); useEffect(() => { if (mode === "discovery" && discovery === null) { fetch("/api/picks/discovery").then(r => r.json()) .then(d => setDiscovery(d.events || [])).catch(() => setDiscovery([])); } }, [mode]); const closeForm = () => { setShowForm(false); setEditId(null); setForm(_EMPTY_FORM); }; const save = () => { if (!form.event_date || !form.title.trim()) return; setSaving(true); const isEdit = editId != null; fetch(isEdit ? "/api/news-events/update" : "/api/news-events", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(isEdit ? { id: editId, ...form } : form), }).then(r => r.json()).then(() => { setSaving(false); closeForm(); load(); }).catch(() => setSaving(false)); }; const startEdit = (ev) => { setForm({ event_date: ev.event_date || "", event_end_date: ev.event_end_date || "", title: ev.title || "", symbol: ev.symbol || "", category: ev.category || "lancio", note: ev.note || "" }); setEditId(ev.id); setShowForm(true); window.scrollTo?.(0, 0); }; const doDelete = () => { const id = confirmDel?.id; setConfirmDel(null); if (id == null) return; setEvents(prev => prev.filter(e => e.id !== id)); // ottimistico fetch("/api/news-events/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }), }).then(() => load()).catch(() => load()); }; // raggruppa per giorno (di INIZIO), futuri prima; un evento multi-giorno // resta visibile finché la sua data di FINE non è passata const visible = events.filter(e => showPast || _daysAway(e.event_end_date || e.event_date) >= 0); const byDay = {}; visible.forEach(e => { (byDay[e.event_date] = byDay[e.event_date] || []).push(e); }); const days = Object.keys(byDay).sort(); const canSave = form.event_date && form.title.trim() && !saving; if (mode === "discovery") { return (
📰 IPO & Spin-off in arrivo
); } return (
{/* barra azioni: l'agenda ha la precedenza, il form è a scomparsa */}
Agenda eventi
{/* form aggiunta/modifica (collassabile) */} {showForm && (
{editId != null && (
✎ Modifica evento
)}
setForm({ ...form, event_date: e.target.value })} />
setForm({ ...form, event_end_date: e.target.value })} />
setForm({ ...form, title: e.target.value })} onKeyDown={e => e.key === "Enter" && canSave && save()} />
setForm({ ...form, symbol: e.target.value.toUpperCase() })} />
{cats.map(c => ( ))}