// Reference detail: media block + summary + metadata + tag attach/detach + linked-from. import { api } from '../api.js'; import { el, mount, clear, safeHref } from '../dom.js'; function mediaBlock(ref) { if (ref.kind === 'image' && (ref.source_url || ref.blob_path)) { const src = safeHref(ref.source_url || ref.blob_path); if (src === '#') return el('span', { class: 'muted' }, '(image url rejected by scheme check)'); return el('img', { src, style: { maxWidth: '100%', borderRadius: '4px', border: '1px solid var(--border)' } }); } if (ref.kind === 'video' && ref.source_url) { const yt = ref.source_url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{6,})/); if (yt) { return el('iframe', { src: 'https://www.youtube-nocookie.com/embed/' + yt[1], style: { width: '100%', aspectRatio: '16/9', border: 'none', borderRadius: '4px' }, allowfullscreen: true, referrerpolicy: 'no-referrer' }); } return el('a', { href: safeHref(ref.source_url), target: '_blank', rel: 'noopener noreferrer' }, ref.source_url); } if (ref.source_url) { return el('a', { href: safeHref(ref.source_url), target: '_blank', rel: 'noopener noreferrer' }, ref.source_url); } return el('span', { class: 'muted' }, '(no source)'); } function metadataTable(ref) { const rows = [ ['kind', ref.kind], ['source_url', ref.source_url], ['captured_at', ref.captured_at], ['status', ref.status], ['source_kind', ref.source_kind], ['external_id', ref.external_id] ].filter(([, v]) => v); return el('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' } }, el('tbody', {}, rows.map(([k, v]) => el('tr', {}, el('td', { class: 'muted', style: { padding: '4px 8px', verticalAlign: 'top', width: '120px' } }, k), el('td', { style: { padding: '4px 8px', wordBreak: 'break-all' } }, String(v)) ) )) ); } async function renderTagsCard(card, ref) { const list = card.querySelector('.tags-list'); clear(list); let tags = []; try { tags = await api.get(`/api/ref/${ref.id}/tags`); } catch { /* leave empty */ } for (const t of tags) { list.appendChild(el('span', { class: 'status idle', style: { marginRight: '6px', cursor: 'pointer' }, title: 'click to detach', onclick: async () => { try { await api.del(`/api/ref/${ref.id}/tags/${t.id}`); renderTagsCard(card, ref); } catch (e) { alert('detach failed: ' + e.message); } } }, t.name)); } } async function tagAttachUI(ref, onChange) { const input = el('input', { type: 'text', placeholder: 'Tag name', style: { flex: 1 } }); async function attach() { const name = input.value.trim(); if (!name) return; try { const tag = await api.post('/api/tags', { name }); await api.post(`/api/ref/${ref.id}/tags`, { tag_id: tag.id }); input.value = ''; onChange(); } catch (e) { alert('attach failed: ' + e.message); } } input.addEventListener('keydown', (e) => { if (e.key === 'Enter') attach(); }); return el('div', { style: { display: 'flex', gap: '8px', marginTop: '8px' } }, input, el('button', { class: 'primary', onclick: attach }, 'Attach') ); } export async function render(main, ctx) { const id = ctx.params.id; mount(main, el('p', { class: 'view-sub muted' }, 'Loading …')); let ref; try { ref = await api.get('/api/refs/' + id); } catch (e) { mount(main, el('h1', { class: 'view-h1' }, 'Reference not found'), el('p', { class: 'view-sub muted' }, e.message) ); return; } const tagsCard = el('div', { class: 'card' }, el('h3', {}, 'Tags'), el('div', { class: 'tags-list' }) ); tagsCard.appendChild(await tagAttachUI(ref, () => renderTagsCard(tagsCard, ref))); const linkedFromCard = el('div', { class: 'card' }, el('h3', {}, 'Linked from'), el('div', { id: 'ref-linked-from' }, el('span', { class: 'muted' }, 'Loading …')) ); mount(main, el('h1', { class: 'view-h1' }, ref.title || '(untitled reference)'), el('p', { class: 'view-sub' }, el('span', { class: 'status idle' }, ref.kind), ref.summary ? ' · ' + ref.summary.slice(0, 200) : '' ), el('div', { class: 'card' }, el('h3', {}, 'Media'), mediaBlock(ref)), ref.summary ? el('div', { class: 'card' }, el('h3', {}, 'Summary'), el('p', {}, ref.summary)) : null, el('div', { class: 'card' }, el('h3', {}, 'Metadata'), metadataTable(ref)), tagsCard, linkedFromCard ); renderTagsCard(tagsCard, ref); try { const links = await api.get('/api/links/to/ref/' + id); const wrap = document.getElementById('ref-linked-from'); if (!links.length) mount(wrap, el('span', { class: 'muted' }, 'Not linked from anywhere yet.')); else mount(wrap, el('ul', { class: 'plain' }, links.map(l => el('li', {}, el('span', { class: 'status idle' }, l.from_type), ' ', l.from_id.slice(0, 8) + '…', ' ', el('span', { class: 'muted' }, 'rel: ' + l.relation) ) )) ); } catch (e) { const wrap = document.getElementById('ref-linked-from'); if (wrap) mount(wrap, el('span', { class: 'muted' }, 'Could not load: ' + e.message)); } }