rspace-online/server/output-list.ts

144 lines
5.0 KiB
TypeScript

/**
* Output list page renderer.
*
* Generates a browsable list page for a module's output type.
* Fetches items from the module's API and renders them as a card grid
* inside the standard rSpace shell.
*/
import type { ModuleInfo, OutputPath } from "../shared/module";
import { renderShell, escapeHtml, escapeAttr } from "./shell";
export function renderOutputListPage(
space: string,
mod: { id: string; name: string; icon: string; description: string },
outputPath: OutputPath,
modules: ModuleInfo[],
): string {
const body = `
<div class="output-list">
<div class="output-list__header">
<span class="output-list__icon">${escapeHtml(outputPath.icon)}</span>
<div>
<h1 class="output-list__title">${escapeHtml(outputPath.name)}</h1>
<p class="output-list__desc">${escapeHtml(outputPath.description)}</p>
</div>
</div>
<div class="output-list__grid" id="output-grid">
<div class="output-list__loading">Loading…</div>
</div>
</div>
<script>
(function() {
var grid = document.getElementById('output-grid');
var space = ${JSON.stringify(space)};
var moduleId = ${JSON.stringify(mod.id)};
var outputPath = ${JSON.stringify(outputPath.path)};
var outputName = ${JSON.stringify(outputPath.name)};
function timeAgo(dateStr) {
if (!dateStr) return '';
var diff = Date.now() - new Date(dateStr).getTime();
var s = Math.floor(diff / 1000);
if (s < 60) return 'just now';
var m = Math.floor(s / 60);
if (m < 60) return m + (m === 1 ? ' minute ago' : ' minutes ago');
var h = Math.floor(m / 60);
if (h < 24) return h + (h === 1 ? ' hour ago' : ' hours ago');
var d = Math.floor(h / 24);
if (d < 30) return d + (d === 1 ? ' day ago' : ' days ago');
var mo = Math.floor(d / 30);
return mo + (mo === 1 ? ' month ago' : ' months ago');
}
function renderCards(items) {
if (!items || items.length === 0) {
grid.innerHTML =
'<div class="output-list__empty">' +
'<div class="output-list__empty-icon">${escapeHtml(outputPath.icon)}</div>' +
'<p>No ' + outputName.toLowerCase() + ' yet</p>' +
'</div>';
return;
}
grid.innerHTML = items.map(function(item) {
var title = item.title || item.name || 'Untitled';
var desc = item.description || item.content_plain || '';
if (desc.length > 120) desc = desc.substring(0, 120) + '…';
var date = item.updated_at || item.created_at || '';
var href = item.url || ('/' + space + '/' + moduleId + '?item=' + (item.id || item.slug || ''));
return '<a class="output-card" href="' + href + '">' +
'<div class="output-card__title">' + escapeText(title) + '</div>' +
(desc ? '<div class="output-card__desc">' + escapeText(desc) + '</div>' : '') +
(date ? '<div class="output-card__date">Updated ' + timeAgo(date) + '</div>' : '') +
'</a>';
}).join('');
}
function escapeText(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
fetch('/' + space + '/' + moduleId + '/api/' + outputPath)
.then(function(r) {
if (!r.ok) throw new Error(r.status);
return r.json();
})
.then(function(data) {
// Try common response shapes: { items: [] }, { [key]: [] }, or array
var items = Array.isArray(data) ? data
: data.items ? data.items
: data[outputPath] ? data[outputPath]
: Object.values(data).find(function(v) { return Array.isArray(v); })
|| [];
renderCards(items);
})
.catch(function() {
renderCards([]);
});
})();
</script>`;
const styles = `<style>
.output-list { max-width: 960px; margin: 0 auto; padding: 2rem 1rem; }
.output-list__header { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; }
.output-list__icon { font-size: 2.5rem; }
.output-list__title { margin: 0; font-size: 1.5rem; color: #f1f5f9; }
.output-list__desc { margin: 0.25rem 0 0; color: #94a3b8; font-size: 0.95rem; }
.output-list__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.output-list__loading { color: #64748b; text-align: center; padding: 3rem 0; grid-column: 1 / -1; }
.output-list__empty { text-align: center; padding: 4rem 1rem; grid-column: 1 / -1; color: #64748b; }
.output-list__empty-icon { font-size: 3rem; margin-bottom: 0.5rem; }
.output-card {
display: block;
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.25rem;
text-decoration: none;
color: inherit;
transition: border-color 0.15s, transform 0.15s;
}
.output-card:hover { border-color: #6366f1; transform: translateY(-2px); }
.output-card__title { font-size: 1.05rem; font-weight: 600; color: #f1f5f9; margin-bottom: 0.5rem; }
.output-card__desc { font-size: 0.875rem; color: #94a3b8; line-height: 1.4; margin-bottom: 0.5rem; }
.output-card__date { font-size: 0.75rem; color: #64748b; }
</style>`;
return renderShell({
title: `${outputPath.name}${mod.name} | rSpace`,
moduleId: mod.id,
spaceSlug: space,
body,
modules,
theme: "dark",
styles,
});
}