components_live_quotes_dialog.js
/**
* LiveQuotesDialog — lists all embed quotes for a project.
*/
export class LiveQuotesDialog {
/**
* @param {string} projectId
* @param {function} getToken - async function returning the current auth token, or null
*/
constructor(projectId, getToken) {
this._projectId = projectId;
this._getToken = getToken ?? (() => Promise.resolve(null));
this._buildDOM();
document.body.appendChild(this.scrim);
document.body.appendChild(this.root);
this._load();
}
_buildDOM() {
this.scrim = document.createElement('div');
this.scrim.className = 'lq-scrim';
this.scrim.addEventListener('click', () => this.close());
this.root = document.createElement('div');
this.root.className = 'lq-dialog';
this.root.innerHTML = `
<div class="lq-header">
<h2 class="lq-title">Live Quotes</h2>
<button class="lq-close-btn" title="Close">✕</button>
</div>
<div class="lq-body">
<div class="lq-list" id="lqList">
<div class="lq-loading">Loading…</div>
</div>
</div>
`;
this.root.querySelector('.lq-close-btn').addEventListener('click', () => this.close());
this._list = this.root.querySelector('#lqList');
}
async _load() {
try {
const token = await this._getToken();
const res = await fetch(`/api/projects/${this._projectId}/embeds`, {
headers: token ? { 'X-Auth-Token': token } : {},
credentials: 'include',
});
if (!res.ok) throw new Error();
const embeds = await res.json();
this._render(embeds);
} catch {
this._list.innerHTML = '<div class="lq-error">Failed to load quotes.</div>';
}
}
_render(embeds) {
if (!embeds.length) {
this._list.innerHTML = '<div class="lq-empty">No live quotes yet. Select transcript text and use the context menu to create one.</div>';
return;
}
this._list.innerHTML = '';
embeds.forEach(embed => {
const url = `${window.location.origin}/embeds/${embed.id}`;
const date = embed.created_at
? new Date(embed.created_at).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
: '—';
const row = document.createElement('div');
row.className = 'lq-row';
row.dataset.embedId = embed.id;
const nameText = embed.name || url;
row.innerHTML = `
<div class="lq-row-info">
<span class="lq-name">${nameText}</span>
<a class="lq-url" href="${url}" target="_blank" rel="noopener">${url}</a>
<div class="lq-meta">
<span class="lq-date">${date}</span>
<span class="lq-dot">·</span>
<span class="lq-views">${embed.invoke_count} ${embed.invoke_count === 1 ? 'view' : 'views'}</span>
</div>
</div>
<div class="lq-row-actions">
<button class="lq-copy-link-btn" title="Copy link">Copy Link</button>
<button class="lq-copy-btn" title="Copy embed code">Copy Code</button>
<button class="lq-delete-btn" title="Delete">Delete</button>
</div>
`;
row.querySelector('.lq-copy-link-btn').addEventListener('click', (e) => {
e.stopPropagation();
this._copyText(url, row.querySelector('.lq-copy-link-btn'));
});
row.querySelector('.lq-copy-btn').addEventListener('click', (e) => {
e.stopPropagation();
this._copyEmbedCode(url, row.querySelector('.lq-copy-btn'));
});
row.querySelector('.lq-delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
this._deleteEmbed(embed.id, row);
});
this._list.appendChild(row);
});
}
_copyText(text, btn) {
navigator.clipboard.writeText(text).then(() => {
const orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = orig; }, 2000);
});
}
_copyEmbedCode(url, btn) {
const code = `<iframe src="${url}" width="100%" height="200" frameborder="0" scrolling="no" style="border:none;overflow:hidden"></iframe>`;
this._copyText(code, btn);
}
async _deleteEmbed(embedId, row) {
const btn = row.querySelector('.lq-delete-btn');
btn.disabled = true;
try {
const token = await this._getToken();
const res = await fetch(`/api/embeds/${embedId}`, {
method: 'DELETE',
headers: token ? { 'X-Auth-Token': token } : {},
credentials: 'include',
});
if (!res.ok) throw new Error();
this._load();
} catch {
btn.disabled = false;
}
}
close() {
this.root.remove();
this.scrim.remove();
}
}