/* 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 : (
);
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
setMode("agenda")}>← Agenda
);
}
return (
{/* barra azioni: l'agenda ha la precedenza, il form è a scomparsa */}
Agenda eventi
setMode("discovery")}
title="Notizie IPO e Spin-off in arrivo (Tool5)">📰 IPO / Spin-off
showForm ? closeForm() : setShowForm(true)}>
{showForm ? "✕ Chiudi" : "+ Aggiungi"}
{/* form aggiunta/modifica (collassabile) */}
{showForm && (
{editId != null && (
✎ Modifica evento
)}
Categoria
{cats.map(c => (
setForm({ ...form, category: c })}>
{(_CAT_EMOJI[c] || "•")} {c}
))}
Nota (opzionale — vai a capo liberamente, usa "-" per gli elenchi)
{saving ? "Salvataggio…" : editId != null ? "Salva modifiche" : "Salva evento"}
{editId != null &&
Annulla }
)}
{visible.length} {visible.length === 1 ? "evento" : "eventi"} {showPast ? "" : "in arrivo"}
setShowPast(!showPast)}>
{showPast ? "Solo futuri" : "Mostra anche passati"}
{err &&
Errore: {err}
}
{days.length === 0 && !err &&
Nessun evento. Aggiungi il primo qui sopra (es. «Lancio iPhone 30» il 25).
}
{days.map(day => {
const away = _daysAway(day);
const past = away < 0;
// giorno passato ma con un evento multi-giorno non ancora finito → "in corso"
const ongoing = past && byDay[day].some(e => e.event_end_date && _daysAway(e.event_end_date) >= 0);
const badge = past ? (ongoing ? "in corso" : "")
: away === 0 ? "oggi" : away === 1 ? "domani" : `tra ${away}g`;
return (
{_fmtDay(day)}
{badge && {badge} }
{byDay[day].map(ev => (
{_CAT_EMOJI[ev.category] || "•"}
{ev.title}
{ev.event_end_date &&
→ {_fmtArtDate(ev.event_end_date)}
}
{(ev.symbol || "").split(",").filter(Boolean).map(s =>
{s} )}
{ev.note &&
{ev.note}
}
startEdit(ev)}>✎
setConfirmDel(ev)}>✕
))}
);
})}
Agenda manuale dei catalizzatori che vuoi seguire (lanci, prodotti, IPO, eventi macro).
Ogni lunedì mattina ricevi su Telegram il riepilogo della settimana, + un promemoria il giorno prima.
{confirmDel && (
setConfirmDel(null)}>
e.stopPropagation()}>
Eliminare questo evento?
{(_CAT_EMOJI[confirmDel.category] || "•")} {confirmDel.title}
{" — "}{_fmtDay(confirmDel.event_date)}. L'azione non è reversibile.
setConfirmDel(null)}>Annulla
Elimina
)}
);
};
})();