diff --git a/frontend/js/views/widgets.js b/frontend/js/views/widgets.js
index 3e5cfa4..c4796c7 100644
--- a/frontend/js/views/widgets.js
+++ b/frontend/js/views/widgets.js
@@ -9,8 +9,122 @@ const WIDGET_TYPES = [
{ id: 'text', name: 'Text/HTML', icon: '📝', desc: 'Custom text or HTML content' },
{ id: 'webpage', name: 'Webpage', icon: '🌐', desc: 'Embed a webpage' },
{ id: 'social', name: 'Social Feed', icon: '💬', desc: 'Social media feed' },
+ { id: 'directory-board', name: 'Directory Board', icon: '🏢', desc: 'Scrolling tenant/room directory for lobbies' },
];
+function escAttr(s) {
+ return String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
+}
+
+function openContentPicker({ multiple = false, title = 'Select Image' } = {}) {
+ return new Promise(async (resolve) => {
+ const overlay = document.createElement('div');
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:10000;padding:16px';
+ overlay.innerHTML = `
+
+
${title}
+
+
+
+
+
+
+ ${multiple ? '' : ''}
+
+
+
`;
+ document.body.appendChild(overlay);
+
+ let items = [];
+ try { items = await API('/content'); } catch {}
+ items = (items || []).filter(i => (i.mime_type || '').startsWith('image/'));
+
+ const selected = new Set();
+ const resolveUrl = (item) => item.remote_url || `/api/content/${item.id}/file`;
+ const updateCount = () => {
+ const el = overlay.querySelector('#cpSelCount');
+ if (el && multiple) el.textContent = `${selected.size} selected`;
+ };
+
+ function renderList() {
+ const q = (overlay.querySelector('#cpSearch').value || '').toLowerCase();
+ const filtered = items.filter(i => (i.filename || '').toLowerCase().includes(q));
+ const list = overlay.querySelector('#cpList');
+ if (!filtered.length) {
+ list.innerHTML = `${items.length ? 'No matches.' : 'No images in your content library. Upload images first from Content Library.'}
`;
+ return;
+ }
+ list.innerHTML = `${
+ filtered.map(c => {
+ const isSel = selected.has(c.id);
+ const thumb = c.remote_url || `/api/content/${c.id}/thumbnail`;
+ return `
+
+
})
+
${escAttr(c.filename)}
+ ${isSel ? '
✓
' : ''}
+
`;
+ }).join('')
+ }
`;
+ list.querySelectorAll('[data-pick-id]').forEach(el => el.onclick = () => {
+ const id = el.dataset.pickId;
+ if (multiple) {
+ if (selected.has(id)) selected.delete(id); else selected.add(id);
+ updateCount();
+ renderList();
+ } else {
+ const item = items.find(x => String(x.id) === id);
+ if (item) { cleanup(); resolve(resolveUrl(item)); }
+ }
+ });
+ }
+
+ function cleanup() { overlay.remove(); }
+
+ overlay.querySelector('#cpSearch').oninput = renderList;
+ overlay.querySelector('#cpCancel').onclick = () => { cleanup(); resolve(multiple ? [] : null); };
+ if (multiple) {
+ overlay.querySelector('#cpDone').onclick = () => {
+ const urls = Array.from(selected).map(id => {
+ const item = items.find(x => String(x.id) === id);
+ return item ? resolveUrl(item) : null;
+ }).filter(Boolean);
+ cleanup();
+ resolve(urls);
+ };
+ }
+ overlay.onclick = (e) => { if (e.target === overlay) { cleanup(); resolve(multiple ? [] : null); } };
+ updateCount();
+ renderList();
+ });
+}
+
+function showPreviewModal(html) {
+ const overlay = document.createElement('div');
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:10000;padding:16px';
+ overlay.innerHTML = `
+
+
+ Preview
+
+
+
+
`;
+ document.body.appendChild(overlay);
+ // srcdoc resolves relative URLs against about:srcdoc, so inject pointing to our origin
+ const baseTag = ``;
+ const withBase = /]*>/i.test(html)
+ ? html.replace(/]*)>/i, `${baseTag}`)
+ : html.replace(/]*)>/i, `${baseTag}`);
+ overlay.querySelector('#pvIframe').srcdoc = withBase;
+ const close = () => overlay.remove();
+ overlay.querySelector('#pvClose').onclick = close;
+ overlay.onclick = (e) => { if (e.target === overlay) close(); };
+ document.addEventListener('keydown', function esc(ev) {
+ if (ev.key === 'Escape') { close(); document.removeEventListener('keydown', esc); }
+ });
+}
+
export async function render(container) {
container.innerHTML = `
-
${w.name}
-
${typeMeta.name || w.widget_type}
+
${escAttr(w.name)}
+
${escAttr(typeMeta.name || w.widget_type)}
-
-
+
+
`;
@@ -201,6 +585,9 @@ export async function render(container) {
}
const deleteBtn = e.target.closest('[data-delete-widget]');
if (deleteBtn) {
+ const w = widgets.find(x => x.id === deleteBtn.dataset.deleteWidget);
+ const label = w ? w.name : 'this widget';
+ if (!confirm(`Delete "${label}"? This cannot be undone.`)) return;
try {
await API(`/widgets/${deleteBtn.dataset.deleteWidget}`, { method: 'DELETE' });
showToast('Widget deleted', 'success');