All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
222 lines
7.7 KiB
JavaScript
222 lines
7.7 KiB
JavaScript
/**
|
||
* Team Activity Log — Surface Entry Point
|
||
*
|
||
* Demonstrates:
|
||
* - SDK boot + sw.* API access
|
||
* - Preact rendering into #extension-mount
|
||
* - Calling Starlark-backed ext API routes (/s/:slug/api/*)
|
||
* - sw.toast, sw.auth, sw.on (WS event subscription)
|
||
* - sw.userMenu for shell integration
|
||
* - Platform CSS variables for theming
|
||
*/
|
||
(async function () {
|
||
'use strict';
|
||
|
||
var mount = document.getElementById('extension-mount');
|
||
if (!mount) return;
|
||
|
||
// ── Boot SDK (extension surfaces don't auto-load it) ──
|
||
var base = window.__BASE__ || '';
|
||
var ver = window.__VERSION__ || '0';
|
||
var slug = (window.__MANIFEST__ || {}).id || 'team-activity-log';
|
||
|
||
try {
|
||
if (!window.preact) {
|
||
var { h, render } = await import(base + '/js/sw/vendor/preact.module.js');
|
||
var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js');
|
||
var htmModule = await import(base + '/js/sw/vendor/htm.module.js');
|
||
window.preact = { h, render };
|
||
window.hooks = hooksModule;
|
||
window.html = htmModule.default.bind(h);
|
||
}
|
||
var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver);
|
||
await sdk.boot();
|
||
} catch (e) {
|
||
mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK boot failed: ' + e.message + '</p>';
|
||
return;
|
||
}
|
||
|
||
var { html } = window;
|
||
var { useState, useEffect, useCallback, useRef } = hooks;
|
||
var { render } = preact;
|
||
|
||
// ── API helper: call Starlark backend ──
|
||
var API_BASE = base + '/s/' + slug + '/api';
|
||
|
||
async function api(method, path, body) {
|
||
var token = sw.auth._getToken();
|
||
var opts = {
|
||
method: method,
|
||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||
};
|
||
if (body) opts.body = JSON.stringify(body);
|
||
var resp = await fetch(API_BASE + path, opts);
|
||
var text = await resp.text();
|
||
try { return JSON.parse(text); } catch (_) { return { _raw: text, _status: resp.status }; }
|
||
}
|
||
|
||
// ── Components ─────────────────────────────
|
||
|
||
var CATEGORIES = ['note', 'update', 'decision', 'action-item'];
|
||
|
||
function App() {
|
||
var [entries, setEntries] = useState([]);
|
||
var [loading, setLoading] = useState(true);
|
||
var [filter, setFilter] = useState('');
|
||
var menuRef = useRef(null);
|
||
|
||
var load = useCallback(async function () {
|
||
var path = '/entries' + (filter ? '?category=' + encodeURIComponent(filter) : '');
|
||
var d = await api('GET', path);
|
||
setEntries(d.data || []);
|
||
setLoading(false);
|
||
}, [filter]);
|
||
|
||
useEffect(function () { load(); }, [load]);
|
||
|
||
// Mount user menu
|
||
useEffect(function () {
|
||
if (menuRef.current && sw.userMenu) {
|
||
sw.userMenu(menuRef.current, { placement: 'down-right' });
|
||
}
|
||
}, []);
|
||
|
||
async function addEntry(category, message) {
|
||
var user = sw.auth.user;
|
||
var result = await api('POST', '/entries', {
|
||
category: category,
|
||
message: message,
|
||
username: user ? (user.display_name || user.username) : '',
|
||
created_at: new Date().toISOString(),
|
||
});
|
||
if (result.error) {
|
||
sw.toast(result.error, 'error');
|
||
return;
|
||
}
|
||
sw.toast('Entry added', 'success');
|
||
load();
|
||
}
|
||
|
||
async function deleteEntry(id) {
|
||
await api('DELETE', '/entries/' + id);
|
||
sw.toast('Entry deleted', 'info');
|
||
load();
|
||
}
|
||
|
||
return html`
|
||
<div class="ext-team-activity-log-shell">
|
||
<header class="ext-team-activity-log-header">
|
||
<div class="ext-team-activity-log-header__left">
|
||
<h1 class="ext-team-activity-log-title">Activity Log</h1>
|
||
<span class="ext-team-activity-log-subtitle">Team activity tracker</span>
|
||
</div>
|
||
<div class="ext-team-activity-log-header__right">
|
||
<div ref=${menuRef}></div>
|
||
</div>
|
||
</header>
|
||
|
||
<${EntryForm} onSubmit=${addEntry} />
|
||
|
||
<div class="ext-team-activity-log-filters">
|
||
<button class="ext-team-activity-log-filter ${filter === '' ? 'ext-team-activity-log-active' : ''}"
|
||
onClick=${() => setFilter('')}>All</button>
|
||
${CATEGORIES.map(c => html`
|
||
<button key=${c}
|
||
class="ext-team-activity-log-filter ${filter === c ? 'ext-team-activity-log-active' : ''}"
|
||
onClick=${() => setFilter(c)}>
|
||
${c}
|
||
</button>
|
||
`)}
|
||
<span class="ext-team-activity-log-count">${entries.length} entries</span>
|
||
</div>
|
||
|
||
${loading
|
||
? html`<div class="ext-team-activity-log-loading">Loading…</div>`
|
||
: html`<${EntryList} entries=${entries} onDelete=${deleteEntry} />`
|
||
}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function EntryForm({ onSubmit }) {
|
||
var [message, setMessage] = useState('');
|
||
var [category, setCategory] = useState('note');
|
||
var [submitting, setSubmitting] = useState(false);
|
||
|
||
async function submit(e) {
|
||
e.preventDefault();
|
||
if (!message.trim()) return;
|
||
setSubmitting(true);
|
||
await onSubmit(category, message.trim());
|
||
setMessage('');
|
||
setSubmitting(false);
|
||
}
|
||
|
||
return html`
|
||
<form class="ext-team-activity-log-form" onSubmit=${submit}>
|
||
<select class="ext-team-activity-log-form__select" value=${category}
|
||
onChange=${e => setCategory(e.target.value)}>
|
||
${CATEGORIES.map(c => html`<option key=${c} value=${c}>${c}</option>`)}
|
||
</select>
|
||
<input class="ext-team-activity-log-form__input" type="text"
|
||
placeholder="What happened?"
|
||
value=${message}
|
||
onInput=${e => setMessage(e.target.value)}
|
||
disabled=${submitting} />
|
||
<button class="ext-team-activity-log-form__btn" type="submit" disabled=${submitting || !message.trim()}>
|
||
${submitting ? '…' : 'Log'}
|
||
</button>
|
||
</form>
|
||
`;
|
||
}
|
||
|
||
function EntryList({ entries, onDelete }) {
|
||
if (!entries.length) {
|
||
return html`<div class="ext-team-activity-log-empty">No entries yet. Log your first activity above.</div>`;
|
||
}
|
||
|
||
return html`
|
||
<div class="ext-team-activity-log-entries">
|
||
${entries.map(e => html`
|
||
<div key=${e.id} class="ext-team-activity-log-entry">
|
||
<div class="ext-team-activity-log-entry__header">
|
||
<span class="ext-team-activity-log-entry__category badge">${e.category}</span>
|
||
<span class="ext-team-activity-log-entry__user">${esc(e.username || 'Unknown')}</span>
|
||
<span class="ext-team-activity-log-entry__time">${timeAgo(e.created_at)}</span>
|
||
<button class="ext-team-activity-log-entry__delete" onClick=${() => onDelete(e.id)}
|
||
title="Delete">×</button>
|
||
</div>
|
||
<div class="ext-team-activity-log-entry__message">${esc(e.message)}</div>
|
||
</div>
|
||
`)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ── Utilities ──────────────────────────────
|
||
|
||
function esc(s) {
|
||
var el = document.createElement('span');
|
||
el.textContent = String(s || '');
|
||
return el.innerHTML;
|
||
}
|
||
|
||
function timeAgo(iso) {
|
||
if (!iso) return '';
|
||
var ms = Date.now() - new Date(iso).getTime();
|
||
var sec = Math.floor(ms / 1000);
|
||
if (sec < 60) return 'just now';
|
||
var min = Math.floor(sec / 60);
|
||
if (min < 60) return min + 'm ago';
|
||
var hr = Math.floor(min / 60);
|
||
if (hr < 24) return hr + 'h ago';
|
||
var d = Math.floor(hr / 24);
|
||
return d + 'd ago';
|
||
}
|
||
|
||
// ── Mount ──────────────────────────────────
|
||
|
||
render(html`<${App} />`, mount);
|
||
console.log('[team-activity-log] Surface mounted');
|
||
})();
|