144 lines
5.0 KiB
TypeScript
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,
|
|
});
|
|
}
|