mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-16 23:33:10 -06:00
121 lines
7.1 KiB
JavaScript
121 lines
7.1 KiB
JavaScript
import { showToast } from '../components/toast.js';
|
|
import { esc } from '../utils.js';
|
|
import { t } from '../i18n.js';
|
|
|
|
const API = (url) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}).then(r => r.json());
|
|
|
|
export async function render(container) {
|
|
container.innerHTML = `
|
|
<div class="page-header">
|
|
<div><h1>${t('activity.title')}</h1><div class="subtitle">${t('activity.subtitle')}</div></div>
|
|
</div>
|
|
<div id="activityList"><div class="empty-state"><h3>${t('common.loading')}</h3></div></div>
|
|
<div style="text-align:center;margin-top:16px">
|
|
<button class="btn btn-secondary btn-sm" id="loadMoreBtn" style="display:none">${t('activity.load_more')}</button>
|
|
</div>
|
|
`;
|
|
|
|
let offset = 0;
|
|
const limit = 50;
|
|
|
|
async function loadActivity(append = false) {
|
|
try {
|
|
const items = await API(`/activity?limit=${limit}&offset=${offset}`);
|
|
const list = document.getElementById('activityList');
|
|
|
|
if (!append) list.innerHTML = '';
|
|
|
|
if (items.length === 0 && offset === 0) {
|
|
list.innerHTML = `<div class="empty-state"><h3>${t('activity.empty_title')}</h3><p>${t('activity.empty_desc')}</p></div>`;
|
|
return;
|
|
}
|
|
|
|
const html = items.map(item => {
|
|
const time = new Date(item.created_at * 1000);
|
|
const timeStr = time.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' +
|
|
time.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
const icon = getActionIcon(item.action);
|
|
|
|
return `
|
|
<div style="display:flex;gap:12px;padding:12px 0;border-bottom:1px solid var(--border);align-items:flex-start">
|
|
<div style="width:32px;height:32px;border-radius:50%;background:var(--bg-card);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px">${icon}</div>
|
|
<div style="flex:1;min-width:0">
|
|
<div style="font-size:13px">
|
|
<strong>${esc(item.user_name || item.user_email || t('activity.system'))}</strong>
|
|
<span style="color:var(--text-secondary)"> ${esc(formatAction(item.action))}</span>
|
|
</div>
|
|
${item.details ? `<div style="font-size:12px;color:var(--text-muted);margin-top:2px">${esc(item.details)}</div>` : ''}
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-muted);white-space:nowrap;flex-shrink:0">${timeStr}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
if (append) {
|
|
list.insertAdjacentHTML('beforeend', html);
|
|
} else {
|
|
list.innerHTML = html;
|
|
}
|
|
|
|
document.getElementById('loadMoreBtn').style.display = items.length >= limit ? '' : 'none';
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
document.getElementById('loadMoreBtn').onclick = () => {
|
|
offset += limit;
|
|
loadActivity(true);
|
|
};
|
|
|
|
loadActivity();
|
|
}
|
|
|
|
function getActionIcon(action) {
|
|
const svgText = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>';
|
|
const svgTrash = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
|
|
const svgEdit = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
|
|
const svgPlus = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
|
|
const svgBell = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
|
|
const svgLink = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>';
|
|
const svgUpload = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>';
|
|
const svgClip = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
|
|
|
|
if (!action) return svgText;
|
|
if (action.includes('DELETE')) return svgTrash;
|
|
if (action.includes('POST') && action.includes('content')) return svgUpload;
|
|
if (action.includes('POST') && action.includes('provision')) return svgLink;
|
|
if (action.includes('POST') && action.includes('assignment')) return svgClip;
|
|
if (action.includes('alert')) return svgBell;
|
|
if (action.includes('PUT')) return svgEdit;
|
|
if (action.includes('POST')) return svgPlus;
|
|
return svgText;
|
|
}
|
|
|
|
// Action verbs are user-visible; translate them through t() so they switch
|
|
// languages with the rest of the UI. The mapping below preserves the original
|
|
// verb-then-noun structure of the English version.
|
|
function formatAction(action) {
|
|
// Verbs
|
|
let s = action
|
|
.replace('POST /api/', t('activity.verb_created') + ' ')
|
|
.replace('PUT /api/', t('activity.verb_updated') + ' ')
|
|
.replace('DELETE /api/', t('activity.verb_deleted') + ' ');
|
|
// Specific endpoints
|
|
s = s
|
|
.replace('/provision/pair', t('activity.action_paired_device'))
|
|
.replace('/content/remote', t('activity.action_added_remote_content'))
|
|
.replace('/content', t('activity.noun_content'))
|
|
.replace('/devices/:id', t('activity.noun_device'))
|
|
.replace('/assignments/device/:deviceId', t('activity.noun_playlist_assignment'))
|
|
.replace('/assignments/:id', t('activity.noun_assignment'))
|
|
.replace('/layouts', t('activity.noun_layout'))
|
|
.replace('/widgets', t('activity.noun_widget'))
|
|
.replace('/schedules', t('activity.noun_schedule'))
|
|
.replace('/walls', t('activity.noun_video_wall'))
|
|
.replace('alert:device_offline', t('activity.alert_device_offline'));
|
|
return s;
|
|
}
|
|
|
|
export function cleanup() {}
|