stackchain-dashboard/frontend/following.js
timmy 041f2dbe9b
Some checks failed
CI / lint (pull_request) Failing after 4m8s
CI / build-release (pull_request) Has been skipped
CI / browser-journey (pull_request) Has been skipped
CI / release-candidate (pull_request) Has been skipped
feat: add mobile Following queue for watched issues (Closes #1293)
2026-08-23 04:04:09 +00:00

109 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function (root, factory) {
const exports = factory();
if (typeof module === 'object' && module.exports) {
module.exports = exports.createFollowing;
module.exports.attachFollowing = exports.attachFollowing;
} else {
root.createFollowing = exports.createFollowing;
root.attachFollowing = exports.attachFollowing;
}
})(typeof self !== 'undefined' ? self : this, function () {
function createFollowing(options) {
let generation = 0;
let snapshot = {revision:0, items:[]};
function publish(status, error) {
const state = {status, revision:snapshot.revision, items:[...snapshot.items]};
if (error) state.error = error;
options.render?.(state);
if (status === 'ready') options.onCount?.(snapshot.items.length);
return state;
}
async function load() {
const requestGeneration = ++generation;
publish('loading');
try {
const result = await options.fetchJson('api/v1/following', {headers:{Accept:'application/json'}});
if (requestGeneration !== generation) return snapshot;
snapshot = {
revision:Number(result?.revision) || 0,
items:Array.isArray(result?.items) ? result.items.slice(0, 50) : [],
};
publish('ready');
return snapshot;
} catch (error) {
if (requestGeneration === generation) publish('error', error);
throw error;
}
}
function open(index) {
const item = snapshot.items[Number(index)];
if (!item) return false;
Promise.resolve(options.onOpen?.({...item, kind:'issue'})).catch(() => {});
return true;
}
return {load, open, count:() => snapshot.items.length};
}
function attachFollowing(onOpen) {
const document = globalThis.document;
const query = selector => document.querySelector(selector);
const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, character =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[character]);
const formatTime = value => new Date(value).toLocaleString();
const fetchJson = async (url, options) => {
const response = await fetch(url, options);
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.detail || payload.error || 'Following is temporarily unavailable.');
return payload;
};
let feature;
function render(state) {
const list = query('#following-list');
const status = query('#following-status');
query('#retry-following').hidden = state.status !== 'error';
if (state.status === 'loading') return void (status.textContent = 'Loading watched issues…');
if (state.status === 'error') return void (status.textContent = state.error?.message || 'Following is temporarily unavailable.');
status.textContent = state.items.length
? state.items.length + (state.items.length === 1 ? ' watched issue.' : ' watched issues.')
: 'No watched issues yet. Watch one from Search to keep it here.';
list.innerHTML = state.items.map((item, index) =>
'<button class="following-card" type="button" data-following-index="' + index + '"><span><strong>' +
escapeHtml(item.title) + '</strong><small>' + escapeHtml(item.repository + ' #' + item.number +
' · ' + item.state + ' · ' + formatTime(item.updated_at)) +
'</small></span><span aria-hidden="true"></span></button>').join('');
list.querySelectorAll('[data-following-index]').forEach(button => button.addEventListener('click', () => {
query('#following-sheet').close();
feature.open(Number(button.dataset.followingIndex));
}));
}
feature = createFollowing({
fetchJson, render,
onCount:count => {
const value = query('[data-mobile-queue-count="following"]');
value.textContent = count;
value.closest('button').setAttribute('aria-label', 'Following, ' + count +
(count === 1 ? ' watched issue' : ' watched issues'));
},
onOpen,
});
query('#close-following').addEventListener('click', () => query('#following-sheet').close());
query('#retry-following').addEventListener('click', () => feature.load().catch(() => {}));
feature.load().catch(() => {});
return {
load:feature.load,
open() {
const sheet = query('#following-sheet');
if (!sheet.open) sheet.showModal();
feature.load().catch(() => {});
return 'opened-following';
},
};
}
return {createFollowing, attachFollowing};
});