Skip to content

Commit 6043043

Browse files
hhkaosclaude
andcommitted
Preview a feed's events before subscribing
Lets people explore a candidate feed's full event list (any layout, real filters) before committing to it, instead of subscribing blind. Confirming adds it as a real subscription; canceling discards it with no trace. (#4) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4e901ad commit 6043043

3 files changed

Lines changed: 146 additions & 21 deletions

File tree

app.js

Lines changed: 136 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
var editingCategoryId = null;
3838
var boardEventContext = null;
3939
var embedReady = false;
40+
var subscribeState = { phase: 'idle' };
41+
var previewMode = null;
4042

4143
var $ = function (id) { return document.getElementById(id); };
4244

@@ -269,27 +271,30 @@
269271
return d;
270272
}
271273

274+
async function fetchOteDocument(url) {
275+
var response = await fetch(url, { headers: { accept: 'application/json,text/html;q=0.9,*/*;q=0.5' } });
276+
if (!response.ok) throw new Error('HTTP ' + response.status);
277+
var type = response.headers.get('content-type') || '';
278+
if (type.indexOf('html') !== -1) {
279+
var html = await response.text();
280+
var discovered = discoverInHtml(html, url);
281+
if (!discovered.length) throw new Error('No encontré cabecera OTE en esa página');
282+
return fetchOteDocument(discovered[0].url);
283+
}
284+
var raw = await response.json();
285+
return { url: url, doc: normaliseDocument(raw, url) };
286+
}
287+
272288
async function loadFeed(feed) {
273289
feed.status = 'loading';
274290
renderLibrary();
275291
try {
276-
var response = await fetch(feed.url, { headers: { accept: 'application/json,text/html;q=0.9,*/*;q=0.5' } });
277-
if (!response.ok) throw new Error('HTTP ' + response.status);
278-
var type = response.headers.get('content-type') || '';
279-
var doc;
280-
if (type.indexOf('html') !== -1) {
281-
var html = await response.text();
282-
var discovered = discoverInHtml(html, feed.url);
283-
if (!discovered.length) throw new Error('No encontré cabecera OTE en esa página');
284-
feed.url = discovered[0].url;
285-
return loadFeed(feed);
286-
}
287-
doc = await response.json();
288-
var normalised = normaliseDocument(doc, feed.url);
289-
feed.title = feed.customTitle || normalised.title;
292+
var result = await fetchOteDocument(feed.url);
293+
feed.url = result.url;
294+
feed.title = feed.customTitle || result.doc.title;
290295
feed.status = 'ok';
291-
feed.updatedAt = normalised.updatedAt || '';
292-
feedCache.set(feed.url, normalised);
296+
feed.updatedAt = result.doc.updatedAt || '';
297+
feedCache.set(feed.url, result.doc);
293298
} catch (error) {
294299
feed.status = 'error';
295300
feed.error = error.message || String(error);
@@ -340,6 +345,9 @@
340345
}
341346

342347
function sourceEvents() {
348+
if (previewMode) {
349+
return (previewMode.doc.events || []).map(function (event) { return inherit(previewMode.doc, event); });
350+
}
343351
var events = allEvents();
344352
if (!state.activeSource || state.activeSource.type === 'all') return events;
345353
if (state.activeSource.type === 'feed') {
@@ -571,12 +579,17 @@
571579
}
572580

573581
function setActiveSource(type, id) {
582+
if (previewMode) {
583+
previewMode = null;
584+
updatePreviewControls();
585+
}
574586
state.activeSource = { type: type, id: id };
575587
persist();
576588
render();
577589
}
578590

579591
function isActiveSource(type, id) {
592+
if (previewMode) return false;
580593
return state.activeSource && state.activeSource.type === type && state.activeSource.id === id;
581594
}
582595

@@ -1090,7 +1103,8 @@
10901103
if (!events.length) {
10911104
widget.events = [];
10921105
widget.setAttribute('empty-message', 'No hay eventos con esos filtros.');
1093-
if (!state.feeds.length) showEmptyState();
1106+
if (previewMode) showMessage('Este feed no tiene eventos.', 'warn', true);
1107+
else if (!state.feeds.length) showEmptyState();
10941108
else showMessage('No hay eventos con esos filtros.', 'warn', true);
10951109
return;
10961110
}
@@ -1110,6 +1124,12 @@
11101124
widget.eventActions = function (context) {
11111125
var event = context.originalEvent;
11121126
if (!event) return [];
1127+
if (previewMode) {
1128+
return [
1129+
{ type: 'link', placement: 'detail' },
1130+
{ type: 'ics', placement: 'detail' }
1131+
];
1132+
}
11131133
var saved = state.boards.some(function (board) { return boardHasEvent(board, event); });
11141134
return [
11151135
{
@@ -1394,6 +1414,96 @@
13941414
loadFeed(feed);
13951415
}
13961416

1417+
function setSubscribeState(next) {
1418+
subscribeState = next;
1419+
renderSubscribeModal();
1420+
}
1421+
1422+
function resetSubscribeModal() {
1423+
$('feed-url').value = '';
1424+
setSubscribeState({ phase: 'idle' });
1425+
}
1426+
1427+
function renderSubscribeModal() {
1428+
var box = $('subscribe-preview');
1429+
var previewButton = $('subscribe-preview-button');
1430+
box.replaceChildren();
1431+
previewButton.disabled = subscribeState.phase === 'loading';
1432+
if (subscribeState.phase === 'idle') {
1433+
box.hidden = true;
1434+
return;
1435+
}
1436+
box.hidden = false;
1437+
if (subscribeState.phase === 'loading') {
1438+
box.appendChild(messageNode('Cargando...'));
1439+
return;
1440+
}
1441+
if (subscribeState.phase === 'error') {
1442+
box.appendChild(messageNode(subscribeState.message, 'warn'));
1443+
}
1444+
}
1445+
1446+
async function handleSubscribePreview() {
1447+
var raw = $('feed-url').value;
1448+
if (!raw.trim()) return;
1449+
var url;
1450+
try { url = normaliseUrl(raw); } catch (e) {
1451+
setSubscribeState({ phase: 'error', message: 'La URL no parece válida.' });
1452+
return;
1453+
}
1454+
var existing = findFeed(url);
1455+
if (existing) {
1456+
closeModal('subscribe-modal');
1457+
loadFeed(existing);
1458+
setActiveSource('feed', existing.url);
1459+
showMessage('Ya estabas suscrito a esta fuente. Actualizando...', 'warn', true);
1460+
return;
1461+
}
1462+
setSubscribeState({ phase: 'loading' });
1463+
try {
1464+
var result = await fetchOteDocument(url);
1465+
closeModal('subscribe-modal');
1466+
enterPreviewMode(result.url, result.doc);
1467+
} catch (error) {
1468+
setSubscribeState({ phase: 'error', message: error.message || String(error) });
1469+
}
1470+
}
1471+
1472+
function enterPreviewMode(url, doc) {
1473+
previewMode = { url: url, doc: doc };
1474+
updatePreviewControls();
1475+
render();
1476+
}
1477+
1478+
function cancelPreview() {
1479+
if (!previewMode) return;
1480+
previewMode = null;
1481+
updatePreviewControls();
1482+
render();
1483+
}
1484+
1485+
function confirmPreviewSubscription() {
1486+
if (!previewMode) return;
1487+
confirmSubscription(previewMode.url, previewMode.doc);
1488+
setActiveSource('feed', previewMode.url);
1489+
}
1490+
1491+
function updatePreviewControls() {
1492+
$('mark-all-read').hidden = Boolean(previewMode);
1493+
$('refresh').hidden = Boolean(previewMode);
1494+
$('preview-confirm-button').hidden = !previewMode;
1495+
$('preview-cancel-button').hidden = !previewMode;
1496+
}
1497+
1498+
function confirmSubscription(url, doc) {
1499+
var feed = { url: url, status: 'ok', title: doc.title, updatedAt: doc.updatedAt || '' };
1500+
state.feeds.push(feed);
1501+
ensureDefaultCategory(feed.url);
1502+
feedCache.set(feed.url, doc);
1503+
persist();
1504+
render();
1505+
}
1506+
13971507
function unsubscribe(rawUrl) {
13981508
var url = normaliseUrl(rawUrl);
13991509
state.feeds = state.feeds.filter(function (feed) { return feed.url !== url; });
@@ -1413,6 +1523,7 @@
14131523
}
14141524

14151525
function sourceLabel() {
1526+
if (previewMode) return previewMode.doc.title || 'Previsualización';
14161527
if (!state.activeSource || state.activeSource.type === 'all') return 'All';
14171528
if (state.activeSource.type === 'feed') {
14181529
var feed = findFeed(state.activeSource.id);
@@ -1439,6 +1550,7 @@
14391550
var modal = $(id);
14401551
if (modal.close) modal.close();
14411552
else modal.removeAttribute('open');
1553+
if (id === 'subscribe-modal') resetSubscribeModal();
14421554
}
14431555

14441556
function bind() {
@@ -1469,10 +1581,14 @@
14691581
});
14701582
$('subscribe-form').addEventListener('submit', function (event) {
14711583
event.preventDefault();
1472-
subscribe($('feed-url').value);
1473-
$('feed-url').value = '';
1474-
closeModal('subscribe-modal');
1584+
handleSubscribePreview();
1585+
});
1586+
$('feed-url').addEventListener('input', function () {
1587+
if (subscribeState.phase !== 'idle') setSubscribeState({ phase: 'idle' });
14751588
});
1589+
$('subscribe-modal').addEventListener('close', resetSubscribeModal);
1590+
$('preview-confirm-button').addEventListener('click', confirmPreviewSubscription);
1591+
$('preview-cancel-button').addEventListener('click', cancelPreview);
14761592
['q', 'mode', 'language', 'country', 'cfp', 'future'].forEach(function (id) {
14771593
$(id).addEventListener('input', function () {
14781594
state.filters[id] = this.type === 'checkbox' ? this.checked : (id === 'q' ? normaliseQuery(this.value) : this.value);

index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ <h2 id="result-count">Eventos</h2>
133133
<div class="results-actions">
134134
<button class="ghost" type="button" id="mark-all-read">Todos vistos</button>
135135
<button class="ghost" type="button" id="refresh">Actualizar</button>
136+
<button type="button" id="preview-confirm-button" hidden>Confirmar suscripción</button>
137+
<button class="ghost" type="button" id="preview-cancel-button" hidden>Cancelar</button>
136138
</div>
137139
</div>
138140
<div id="messages"></div>
@@ -152,8 +154,9 @@ <h2>Nueva suscripción</h2>
152154
<label for="feed-url">Feed OTE o web con cabecera OTE</label>
153155
<div class="url-row">
154156
<input id="feed-url" name="url" type="url" placeholder="https://comunidad.example/events.json" autocomplete="url">
155-
<button type="submit">Añadir</button>
157+
<button type="submit" id="subscribe-preview-button">Previsualizar</button>
156158
</div>
159+
<div class="subscribe-preview" id="subscribe-preview" hidden></div>
157160
<p>También puedes abrir OTE Reader con <code>?subscribe=https://...</code>. La autodetección de webs depende de CORS hasta que exista extensión.</p>
158161
</form>
159162
</dialog>

styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,9 @@ label {
10101010
gap: 8px;
10111011
margin-top: 16px;
10121012
}
1013+
.modal-actions[hidden] {
1014+
display: none;
1015+
}
10131016
.modal-actions [hidden] {
10141017
display: none;
10151018
}
@@ -1050,6 +1053,9 @@ label {
10501053
text-overflow: ellipsis;
10511054
white-space: nowrap;
10521055
}
1056+
.subscribe-preview {
1057+
margin-top: 12px;
1058+
}
10531059
.board-picker {
10541060
display: grid;
10551061
gap: 6px;

0 commit comments

Comments
 (0)