Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m28s
Details
CI/CD / deploy (push) Successful in 2m28s
Details
This commit is contained in:
commit
c2fb1b2858
|
|
@ -362,6 +362,29 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
if (name === 'view' && (val === 'canvas' || val === 'collaborate' || val === 'dashboard')) this.currentView = val;
|
if (name === 'view' && (val === 'canvas' || val === 'collaborate' || val === 'dashboard')) this.currentView = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onShellTabChange = (e: Event) => {
|
||||||
|
const tab = (e as CustomEvent).detail?.tab;
|
||||||
|
if (!tab) return;
|
||||||
|
if (tab === 'pool') this._switchView('canvas');
|
||||||
|
else if (tab === 'fulfillment') this._switchView('dashboard');
|
||||||
|
// 'cyclos' tab is a redirect, no client-side handling needed
|
||||||
|
};
|
||||||
|
|
||||||
|
private _switchView(view: 'canvas' | 'collaborate' | 'dashboard') {
|
||||||
|
if (view === this.currentView) return;
|
||||||
|
this._history.push(view);
|
||||||
|
this.currentView = view;
|
||||||
|
const canvasView = this.shadow.getElementById('canvas-view');
|
||||||
|
const collabView = this.shadow.getElementById('collaborate-view');
|
||||||
|
const dashView = this.shadow.getElementById('dashboard-view');
|
||||||
|
if (canvasView) canvasView.style.display = view === 'canvas' ? 'flex' : 'none';
|
||||||
|
if (collabView) collabView.style.display = view === 'collaborate' ? 'flex' : 'none';
|
||||||
|
if (dashView) dashView.style.display = view === 'dashboard' ? 'flex' : 'none';
|
||||||
|
if (view === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); }
|
||||||
|
if (view === 'collaborate') this.refreshCollaborate();
|
||||||
|
if (view === 'dashboard') this.refreshDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.space = this.getAttribute('space') || 'demo';
|
this.space = this.getAttribute('space') || 'demo';
|
||||||
const rawView = this.getAttribute('view');
|
const rawView = this.getAttribute('view');
|
||||||
|
|
@ -383,6 +406,7 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' }));
|
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' }));
|
||||||
if (this.space !== 'demo') this.subscribeOffline();
|
if (this.space !== 'demo') this.subscribeOffline();
|
||||||
|
document.addEventListener('rapp-tab-change', this._onShellTabChange);
|
||||||
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
|
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,6 +445,7 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this._history.destroy();
|
this._history.destroy();
|
||||||
|
document.removeEventListener('rapp-tab-change', this._onShellTabChange);
|
||||||
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
|
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
|
||||||
if (this.animFrame) cancelAnimationFrame(this.animFrame);
|
if (this.animFrame) cancelAnimationFrame(this.animFrame);
|
||||||
this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null;
|
this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null;
|
||||||
|
|
@ -435,18 +460,7 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
|
|
||||||
private _onViewRestored = (e: CustomEvent) => {
|
private _onViewRestored = (e: CustomEvent) => {
|
||||||
if (e.detail?.moduleId !== 'rtime') return;
|
if (e.detail?.moduleId !== 'rtime') return;
|
||||||
this.currentView = e.detail.view;
|
this._switchView(e.detail.view);
|
||||||
// Toggle visibility of view panels
|
|
||||||
const canvasView = this.shadow.getElementById('canvas-view');
|
|
||||||
const collabView = this.shadow.getElementById('collaborate-view');
|
|
||||||
const dashView = this.shadow.getElementById('dashboard-view');
|
|
||||||
if (canvasView) canvasView.style.display = this.currentView === 'canvas' ? 'flex' : 'none';
|
|
||||||
if (collabView) collabView.style.display = this.currentView === 'collaborate' ? 'flex' : 'none';
|
|
||||||
if (dashView) dashView.style.display = this.currentView === 'dashboard' ? 'flex' : 'none';
|
|
||||||
this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === this.currentView));
|
|
||||||
if (this.currentView === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); }
|
|
||||||
if (this.currentView === 'collaborate') this.refreshCollaborate();
|
|
||||||
if (this.currentView === 'dashboard') this.refreshDashboard();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private async fetchData() {
|
private async fetchData() {
|
||||||
|
|
@ -528,18 +542,6 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
private render() {
|
private render() {
|
||||||
this.shadow.innerHTML = `
|
this.shadow.innerHTML = `
|
||||||
<style>${CSS_TEXT}</style>
|
<style>${CSS_TEXT}</style>
|
||||||
<div class="tab-bar">
|
|
||||||
<div class="tab active" data-view="canvas">Canvas</div>
|
|
||||||
<div class="tab" data-view="collaborate">Collaborate</div>
|
|
||||||
<div class="tab" data-view="dashboard">Fulfillment</div>
|
|
||||||
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark theme">☀</button>
|
|
||||||
</div>
|
|
||||||
<div class="stats-bar">
|
|
||||||
<div class="stat"><span class="stat-value" id="statHours">0</span> hours available</div>
|
|
||||||
<div class="stat"><span class="stat-value" id="statContributors">0</span> contributors</div>
|
|
||||||
<div class="skill-bar" id="skillBar"></div>
|
|
||||||
<div class="skill-legend" id="skillLegend"></div>
|
|
||||||
</div>
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div id="canvas-view">
|
<div id="canvas-view">
|
||||||
<!-- Left pool panel -->
|
<!-- Left pool panel -->
|
||||||
|
|
@ -547,9 +549,18 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
<div class="pool-panel-header">
|
<div class="pool-panel-header">
|
||||||
<span>Commitment Pool</span>
|
<span>Commitment Pool</span>
|
||||||
<span class="pool-hint">drag orb to weave \u2192</span>
|
<span class="pool-hint">drag orb to weave \u2192</span>
|
||||||
|
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark theme">☀</button>
|
||||||
<button id="poolPanelToggle" title="Collapse panel">\u27E8</button>
|
<button id="poolPanelToggle" title="Collapse panel">\u27E8</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="pool-canvas"></canvas>
|
<canvas id="pool-canvas"></canvas>
|
||||||
|
<div class="pool-legend" id="poolLegend">
|
||||||
|
<div class="pool-legend-stats">
|
||||||
|
<span><strong id="statHours">0</strong> hours</span>
|
||||||
|
<span class="pool-legend-sep">\u00b7</span>
|
||||||
|
<span><strong id="statContributors">0</strong> contributors</span>
|
||||||
|
</div>
|
||||||
|
<div class="pool-legend-skills" id="skillLegend"></div>
|
||||||
|
</div>
|
||||||
<div class="pool-detail" id="poolDetail">
|
<div class="pool-detail" id="poolDetail">
|
||||||
<div class="pool-detail-header">
|
<div class="pool-detail-header">
|
||||||
<div class="pool-detail-dot" id="detailDot"></div>
|
<div class="pool-detail-dot" id="detailDot"></div>
|
||||||
|
|
@ -781,26 +792,6 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
this.canvas = this.shadow.getElementById('pool-canvas') as HTMLCanvasElement;
|
this.canvas = this.shadow.getElementById('pool-canvas') as HTMLCanvasElement;
|
||||||
this.ctx = this.canvas.getContext('2d')!;
|
this.ctx = this.canvas.getContext('2d')!;
|
||||||
|
|
||||||
// Tab switching (3 tabs: canvas, collaborate, dashboard)
|
|
||||||
this.shadow.querySelectorAll('.tab').forEach(tab => {
|
|
||||||
tab.addEventListener('click', () => {
|
|
||||||
const view = (tab as HTMLElement).dataset.view as 'canvas' | 'collaborate' | 'dashboard';
|
|
||||||
if (view === this.currentView) return;
|
|
||||||
this._history.push(view);
|
|
||||||
this.currentView = view;
|
|
||||||
this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === view));
|
|
||||||
const canvasView = this.shadow.getElementById('canvas-view')!;
|
|
||||||
const collabView = this.shadow.getElementById('collaborate-view')!;
|
|
||||||
const dashView = this.shadow.getElementById('dashboard-view')!;
|
|
||||||
canvasView.style.display = view === 'canvas' ? 'flex' : 'none';
|
|
||||||
collabView.style.display = view === 'collaborate' ? 'flex' : 'none';
|
|
||||||
dashView.style.display = view === 'dashboard' ? 'flex' : 'none';
|
|
||||||
if (view === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); }
|
|
||||||
if (view === 'collaborate') this.refreshCollaborate();
|
|
||||||
if (view === 'dashboard') this.refreshDashboard();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Theme toggle
|
// Theme toggle
|
||||||
this.shadow.getElementById('themeToggle')!.addEventListener('click', () => {
|
this.shadow.getElementById('themeToggle')!.addEventListener('click', () => {
|
||||||
this._theme = this._theme === 'dark' ? 'light' : 'dark';
|
this._theme = this._theme === 'dark' ? 'light' : 'dark';
|
||||||
|
|
@ -1222,20 +1213,17 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
|
|
||||||
private updateStats() {
|
private updateStats() {
|
||||||
const total = this.commitments.reduce((s, c) => s + c.hours, 0);
|
const total = this.commitments.reduce((s, c) => s + c.hours, 0);
|
||||||
this.shadow.getElementById('statHours')!.textContent = String(total);
|
const hoursEl = this.shadow.getElementById('statHours');
|
||||||
this.shadow.getElementById('statContributors')!.textContent = String(this.commitments.length);
|
const contribEl = this.shadow.getElementById('statContributors');
|
||||||
|
if (hoursEl) hoursEl.textContent = String(total);
|
||||||
|
if (contribEl) contribEl.textContent = String(this.commitments.length);
|
||||||
const bySkill: Record<string, number> = {};
|
const bySkill: Record<string, number> = {};
|
||||||
this.commitments.forEach(c => { bySkill[c.skill] = (bySkill[c.skill] || 0) + c.hours; });
|
this.commitments.forEach(c => { bySkill[c.skill] = (bySkill[c.skill] || 0) + c.hours; });
|
||||||
const bar = this.shadow.getElementById('skillBar')!;
|
const legend = this.shadow.getElementById('skillLegend');
|
||||||
const legend = this.shadow.getElementById('skillLegend')!;
|
if (!legend) return;
|
||||||
bar.innerHTML = ''; legend.innerHTML = '';
|
legend.innerHTML = '';
|
||||||
if (total === 0) return;
|
if (total === 0) return;
|
||||||
for (const [skill, hrs] of Object.entries(bySkill)) {
|
for (const [skill, hrs] of Object.entries(bySkill)) {
|
||||||
const seg = document.createElement('div');
|
|
||||||
seg.className = 'skill-bar-segment';
|
|
||||||
seg.style.width = ((hrs as number) / total * 100) + '%';
|
|
||||||
seg.style.background = SKILL_COLORS[skill] || '#888';
|
|
||||||
bar.appendChild(seg);
|
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'skill-legend-item';
|
item.className = 'skill-legend-item';
|
||||||
item.innerHTML = '<div class="skill-legend-dot" style="background:' + (SKILL_COLORS[skill] || '#888') + '"></div>' + (SKILL_LABELS[skill] || skill) + ' ' + hrs + 'h';
|
item.innerHTML = '<div class="skill-legend-dot" style="background:' + (SKILL_COLORS[skill] || '#888') + '"></div>' + (SKILL_LABELS[skill] || skill) + ' ' + hrs + 'h';
|
||||||
|
|
@ -3040,10 +3028,10 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
if (this._tour) return;
|
if (this._tour) return;
|
||||||
const { TourEngine } = await import('../../../shared/tour-engine');
|
const { TourEngine } = await import('../../../shared/tour-engine');
|
||||||
this._tour = new TourEngine(this.shadow as unknown as ShadowRoot, [
|
this._tour = new TourEngine(this.shadow as unknown as ShadowRoot, [
|
||||||
{ target: '.tab-bar', title: 'Canvas & Collaborate', message: 'The Canvas shows commitment orbs alongside the weaving SVG. Collaborate matches intents via the solver.' },
|
{ target: '.pool-panel-header', title: 'Canvas & Collaborate', message: 'The Canvas shows commitment orbs alongside the weaving SVG. Collaborate matches intents via the solver.' },
|
||||||
{ target: '#pool-canvas', title: 'Commitment Pool', message: 'Each floating orb represents a time commitment — sized by hours, colored by skill. Long-press to drag onto the canvas.' },
|
{ target: '#pool-canvas', title: 'Commitment Pool', message: 'Each floating orb represents a time commitment — sized by hours, colored by skill. Long-press to drag onto the canvas.' },
|
||||||
{ target: '#addBtn', title: 'Add a Commitment', message: 'Pledge your hours with a skill category. Your commitment joins the pool for others to see.', advanceOnClick: true },
|
{ target: '#addBtn', title: 'Add a Commitment', message: 'Pledge your hours with a skill category. Your commitment joins the pool for others to see.', advanceOnClick: true },
|
||||||
{ target: '.stats-bar', title: 'Community Stats', message: 'See total hours available and how many contributors are in the pool at a glance.' },
|
{ target: '.pool-legend', title: 'Community Stats', message: 'See total hours available and how many contributors are in the pool at a glance.' },
|
||||||
], 'rtime_tour_done', () => this.shadow.querySelector('.main') as HTMLElement);
|
], 'rtime_tour_done', () => this.shadow.querySelector('.main') as HTMLElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3400,12 +3388,7 @@ class FolkTimebankApp extends HTMLElement {
|
||||||
this.rebuildSidebar();
|
this.rebuildSidebar();
|
||||||
|
|
||||||
// Switch to canvas view
|
// Switch to canvas view
|
||||||
this.currentView = 'canvas';
|
this._switchView('canvas');
|
||||||
this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === 'canvas'));
|
|
||||||
this.shadow.getElementById('canvas-view')!.style.display = 'flex';
|
|
||||||
this.shadow.getElementById('collaborate-view')!.style.display = 'none';
|
|
||||||
const dashView = this.shadow.getElementById('dashboard-view');
|
|
||||||
if (dashView) dashView.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSkillPrices() {
|
private renderSkillPrices() {
|
||||||
|
|
@ -3588,42 +3571,22 @@ const CSS_TEXT = `
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-bar {
|
.pool-legend {
|
||||||
display: flex;
|
padding: 0.5rem 0.75rem;
|
||||||
background: #1e293b;
|
background: rgba(30, 27, 75, 0.6);
|
||||||
border-bottom: 1px solid #334155;
|
border-top: 1px solid #334155;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.tab {
|
.pool-legend-stats {
|
||||||
padding: 0.65rem 1.5rem;
|
display: flex; align-items: center; gap: 0.4rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.78rem; color: #94a3b8; margin-bottom: 0.3rem;
|
||||||
font-weight: 500;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
.tab:hover { color: #e2e8f0; }
|
.pool-legend-stats strong { font-weight: 700; color: #f1f5f9; }
|
||||||
.tab.active { color: #8b5cf6; border-bottom-color: #8b5cf6; }
|
.pool-legend-sep { color: #475569; }
|
||||||
|
.pool-legend-skills { display: flex; gap: 0.6rem; flex-wrap: wrap; }
|
||||||
.stats-bar {
|
.skill-legend { display: flex; gap: 0.6rem; flex-wrap: wrap; }
|
||||||
display: flex;
|
.skill-legend-item { display: flex; align-items: center; gap: 0.25rem; font-size: 0.7rem; color: #94a3b8; }
|
||||||
align-items: center;
|
.skill-legend-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||||
gap: 1.5rem;
|
|
||||||
padding: 0.6rem 1.5rem;
|
|
||||||
background: linear-gradient(135deg, #1e1b4b 0%, #2d1b3d 100%);
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.stat { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: #94a3b8; }
|
|
||||||
.stat-value { font-weight: 700; font-size: 1rem; color: #f1f5f9; }
|
|
||||||
.skill-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; flex: 1; min-width: 150px; max-width: 300px; }
|
|
||||||
.skill-bar-segment { transition: width 0.5s ease; }
|
|
||||||
.skill-legend { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
|
||||||
.skill-legend-item { display: flex; align-items: center; gap: 0.3rem; font-size: 0.75rem; color: #94a3b8; }
|
|
||||||
.skill-legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
||||||
|
|
||||||
.main { flex: 1; position: relative; overflow: hidden; }
|
.main { flex: 1; position: relative; overflow: hidden; }
|
||||||
|
|
||||||
|
|
@ -4320,11 +4283,10 @@ const CSS_TEXT = `
|
||||||
|
|
||||||
/* Light theme overrides */
|
/* Light theme overrides */
|
||||||
:host([data-theme="light"]) { background: #f8fafc; color: #1e293b; }
|
:host([data-theme="light"]) { background: #f8fafc; color: #1e293b; }
|
||||||
:host([data-theme="light"]) .tab-bar { background: #fff; border-bottom-color: #e2e8f0; }
|
:host([data-theme="light"]) .pool-legend { background: rgba(240, 236, 255, 0.6); border-top-color: #e2e8f0; }
|
||||||
:host([data-theme="light"]) .tab { color: #94a3b8; }
|
:host([data-theme="light"]) .pool-legend-stats { color: #64748b; }
|
||||||
:host([data-theme="light"]) .tab:hover { color: #1e293b; }
|
:host([data-theme="light"]) .skill-legend-item { color: #64748b; }
|
||||||
:host([data-theme="light"]) .tab.active { color: #8b5cf6; }
|
:host([data-theme="light"]) .pool-legend-stats strong { color: #1e293b; }
|
||||||
:host([data-theme="light"]) .stats-bar { background: linear-gradient(135deg, #f0ecff 0%, #fdf2f8 100%); border-bottom-color: #e2e8f0; }
|
|
||||||
:host([data-theme="light"]) .stat { color: #64748b; }
|
:host([data-theme="light"]) .stat { color: #64748b; }
|
||||||
:host([data-theme="light"]) .stat-value { color: #1e293b; }
|
:host([data-theme="light"]) .stat-value { color: #1e293b; }
|
||||||
:host([data-theme="light"]) .pool-panel { background: #fff; border-right-color: #e2e8f0; }
|
:host([data-theme="light"]) .pool-panel { background: #fff; border-right-color: #e2e8f0; }
|
||||||
|
|
@ -4407,9 +4369,7 @@ const CSS_TEXT = `
|
||||||
.exec-step-checklist input[type="checkbox"] { accent-color: #8b5cf6; }
|
.exec-step-checklist input[type="checkbox"] { accent-color: #8b5cf6; }
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.tab-bar { gap: 0; }
|
.pool-legend { padding: 0.35rem 0.5rem; }
|
||||||
.tab { padding: 0.45rem 1rem; font-size: 0.82rem; }
|
|
||||||
.stats-bar { padding: 0.35rem 0.75rem; gap: 0.75rem; }
|
|
||||||
.stat { font-size: 0.75rem; }
|
.stat { font-size: 0.75rem; }
|
||||||
.stat-value { font-size: 0.85rem; }
|
.stat-value { font-size: 0.85rem; }
|
||||||
.skill-bar { min-width: 80px; max-width: 160px; }
|
.skill-bar { min-width: 80px; max-width: 160px; }
|
||||||
|
|
@ -4434,9 +4394,7 @@ const CSS_TEXT = `
|
||||||
.task-edit-panel { width: 95vw; }
|
.task-edit-panel { width: 95vw; }
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.tab { padding: 0.4rem 0.75rem; font-size: 0.78rem; }
|
.pool-legend-skills { display: none; }
|
||||||
.stats-bar { padding: 0.3rem 0.5rem; gap: 0.5rem; }
|
|
||||||
.skill-legend { display: none; }
|
|
||||||
.pool-panel.collapsed { width: 100% !important; }
|
.pool-panel.collapsed { width: 100% !important; }
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1108,19 +1108,49 @@ routes.post("/api/cyclos/transfers", async (c) => {
|
||||||
|
|
||||||
// ── Page routes ──
|
// ── Page routes ──
|
||||||
|
|
||||||
routes.get("/", (c) => {
|
// ── Shell tabs for rTime ──
|
||||||
const space = c.req.param("space") || "demo";
|
const RTIME_TABS = [
|
||||||
|
{ id: "pool", label: "Commitment Pool" },
|
||||||
|
{ id: "fulfillment", label: "Fulfillment" },
|
||||||
|
{ id: "cyclos", label: "Open in Cyclos" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
return c.html(renderShell({
|
const RTIME_TAB_IDS = new Set(RTIME_TABS.map(t => t.id));
|
||||||
|
|
||||||
|
function renderTimePage(space: string, view: string, activeTab: string, isSubdomain: boolean) {
|
||||||
|
return renderShell({
|
||||||
title: `${space} — rTime | rSpace`,
|
title: `${space} — rTime | rSpace`,
|
||||||
moduleId: "rtime",
|
moduleId: "rtime",
|
||||||
spaceSlug: space,
|
spaceSlug: space,
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: `<folk-timebank-app space="${space}" view="pool"></folk-timebank-app>`,
|
body: `<folk-timebank-app space="${space}" view="${view}"></folk-timebank-app>`,
|
||||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=2"></script>`,
|
||||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
||||||
}));
|
tabs: [...RTIME_TABS],
|
||||||
|
activeTab,
|
||||||
|
tabBasePath: isSubdomain ? `/rtime` : `/${space}/rtime`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.get("/pool", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
return c.html(renderTimePage(space, "canvas", "pool", c.get("isSubdomain")));
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.get("/fulfillment", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
return c.html(renderTimePage(space, "dashboard", "fulfillment", c.get("isSubdomain")));
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.get("/cyclos", (c) => {
|
||||||
|
const cyclosUrl = process.env.CYCLOS_URL || "https://www.cyclos.org";
|
||||||
|
return c.redirect(cyclosUrl, 302);
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.get("/", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
return c.html(renderTimePage(space, "canvas", "pool", c.get("isSubdomain")));
|
||||||
});
|
});
|
||||||
|
|
||||||
routes.post("/api/tasks/:id/export-to-backlog", async (c) => {
|
routes.post("/api/tasks/:id/export-to-backlog", async (c) => {
|
||||||
|
|
@ -1175,49 +1205,18 @@ routes.post("/api/tasks/:id/export-to-backlog", async (c) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legacy routes — redirect to new tab paths
|
||||||
routes.get("/canvas", (c) => {
|
routes.get("/canvas", (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
|
return c.redirect(c.get("isSubdomain") ? `/rtime/pool` : `/${space}/rtime/pool`, 301);
|
||||||
return c.html(renderShell({
|
|
||||||
title: `${space} — Canvas | rTime | rSpace`,
|
|
||||||
moduleId: "rtime",
|
|
||||||
spaceSlug: space,
|
|
||||||
modules: getModuleInfoList(),
|
|
||||||
theme: "dark",
|
|
||||||
body: `<folk-timebank-app space="${space}" view="canvas"></folk-timebank-app>`,
|
|
||||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
|
||||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
routes.get("/collaborate", (c) => {
|
routes.get("/collaborate", (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
|
return c.redirect(c.get("isSubdomain") ? `/rtime/pool` : `/${space}/rtime/pool`, 301);
|
||||||
return c.html(renderShell({
|
|
||||||
title: `${space} — Collaborate | rTime | rSpace`,
|
|
||||||
moduleId: "rtime",
|
|
||||||
spaceSlug: space,
|
|
||||||
modules: getModuleInfoList(),
|
|
||||||
theme: "dark",
|
|
||||||
body: `<folk-timebank-app space="${space}" view="collaborate"></folk-timebank-app>`,
|
|
||||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
|
||||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
routes.get("/dashboard", (c) => {
|
routes.get("/dashboard", (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
|
return c.redirect(c.get("isSubdomain") ? `/rtime/fulfillment` : `/${space}/rtime/fulfillment`, 301);
|
||||||
return c.html(renderShell({
|
|
||||||
title: `${space} — Fulfillment | rTime | rSpace`,
|
|
||||||
moduleId: "rtime",
|
|
||||||
spaceSlug: space,
|
|
||||||
modules: getModuleInfoList(),
|
|
||||||
theme: "dark",
|
|
||||||
body: `<folk-timebank-app space="${space}" view="dashboard"></folk-timebank-app>`,
|
|
||||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
|
||||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Module export ──
|
// ── Module export ──
|
||||||
|
|
|
||||||
|
|
@ -871,6 +871,9 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valid module IDs for this space (used to filter out stale/phantom tabs)
|
||||||
|
const validModuleIds = new Set(moduleList.map(m => m.id));
|
||||||
|
|
||||||
// ── Restore tabs from localStorage ──
|
// ── Restore tabs from localStorage ──
|
||||||
let layers;
|
let layers;
|
||||||
try {
|
try {
|
||||||
|
|
@ -879,12 +882,15 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
if (!Array.isArray(layers)) layers = [];
|
if (!Array.isArray(layers)) layers = [];
|
||||||
} catch(e) { layers = []; }
|
} catch(e) { layers = []; }
|
||||||
|
|
||||||
|
// Filter out stale tabs whose moduleId no longer exists
|
||||||
|
layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
|
||||||
|
|
||||||
// Ensure the current module is in the tab list
|
// Ensure the current module is in the tab list
|
||||||
if (!layers.find(l => l.moduleId === currentModuleId)) {
|
if (!layers.find(l => l.moduleId === currentModuleId)) {
|
||||||
layers.push(makeLayer(currentModuleId, layers.length));
|
layers.push(makeLayer(currentModuleId, layers.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist immediately (includes the newly-added tab)
|
// Persist immediately (cleaned list)
|
||||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||||
|
|
||||||
// Render all tabs with the current one active
|
// Render all tabs with the current one active
|
||||||
|
|
@ -954,7 +960,7 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
if (!next.has(mid) && tabCache) tabCache.removePane(mid);
|
if (!next.has(mid) && tabCache) tabCache.removePane(mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
layers = deduplicateLayers(remoteLayers);
|
layers = deduplicateLayers(remoteLayers.filter(l => validModuleIds.has(l.moduleId)));
|
||||||
|
|
||||||
// Always ensure the current module stays in the tab list
|
// Always ensure the current module stays in the tab list
|
||||||
if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) {
|
if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) {
|
||||||
|
|
@ -1001,7 +1007,8 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
// Server-authoritative: adopt server tabs exactly.
|
// Server-authoritative: adopt server tabs exactly.
|
||||||
// Only skip tabs the user closed in THIS session (prevents
|
// Only skip tabs the user closed in THIS session (prevents
|
||||||
// close-then-immediate-resurrect before save propagates).
|
// close-then-immediate-resurrect before save propagates).
|
||||||
var serverTabs = data.tabs.filter(t => !_closedModuleIds.has(t.moduleId));
|
var serverTabs = data.tabs.filter(t => validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId));
|
||||||
|
serverTabs = deduplicateLayers(serverTabs);
|
||||||
serverTabs.forEach((l, i) => { l.order = i; });
|
serverTabs.forEach((l, i) => { l.order = i; });
|
||||||
layers = serverTabs;
|
layers = serverTabs;
|
||||||
// Ensure the currently navigated module is present
|
// Ensure the currently navigated module is present
|
||||||
|
|
@ -1711,6 +1718,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
|
|
||||||
if (tabBar) {
|
if (tabBar) {
|
||||||
tabBar.setModules(moduleList);
|
tabBar.setModules(moduleList);
|
||||||
|
const validModuleIds = new Set(moduleList.map(m => m.id));
|
||||||
function getModuleLabel(id) {
|
function getModuleLabel(id) {
|
||||||
const m = moduleList.find(mod => mod.id === id);
|
const m = moduleList.find(mod => mod.id === id);
|
||||||
return m ? m.name : id;
|
return m ? m.name : id;
|
||||||
|
|
@ -1718,13 +1726,20 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
function makeLayer(id, order) {
|
function makeLayer(id, order) {
|
||||||
return { id: 'layer-' + id, moduleId: id, label: getModuleLabel(id), order, color: '', visible: true, createdAt: Date.now() };
|
return { id: 'layer-' + id, moduleId: id, label: getModuleLabel(id), order, color: '', visible: true, createdAt: Date.now() };
|
||||||
}
|
}
|
||||||
|
function deduplicateLayers(list) {
|
||||||
|
const seen = new Set();
|
||||||
|
return list.filter(l => { if (seen.has(l.moduleId)) return false; seen.add(l.moduleId); return true; });
|
||||||
|
}
|
||||||
let layers;
|
let layers;
|
||||||
try { const saved = localStorage.getItem(TABS_KEY); layers = saved ? JSON.parse(saved) : []; if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; }
|
try { const saved = localStorage.getItem(TABS_KEY); layers = saved ? JSON.parse(saved) : []; if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; }
|
||||||
|
// Filter out stale/phantom tabs and deduplicate
|
||||||
|
layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
|
||||||
if (!layers.find(l => l.moduleId === currentModuleId)) layers.push(makeLayer(currentModuleId, layers.length));
|
if (!layers.find(l => l.moduleId === currentModuleId)) layers.push(makeLayer(currentModuleId, layers.length));
|
||||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||||
tabBar.setLayers(layers);
|
tabBar.setLayers(layers);
|
||||||
tabBar.setAttribute('active', 'layer-' + currentModuleId);
|
tabBar.setAttribute('active', 'layer-' + currentModuleId);
|
||||||
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
|
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
|
||||||
|
const _closedModuleIds = new Set();
|
||||||
function saveTabs() {
|
function saveTabs() {
|
||||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||||
try {
|
try {
|
||||||
|
|
@ -1754,7 +1769,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
.then(function(r) { return r.ok ? r.json() : null; })
|
.then(function(r) { return r.ok ? r.json() : null; })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; }
|
if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; }
|
||||||
layers = data.tabs;
|
// Filter stale moduleIds and tabs closed this session
|
||||||
|
layers = deduplicateLayers(data.tabs.filter(function(t) { return validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId); }));
|
||||||
layers.forEach(function(l, i) { l.order = i; });
|
layers.forEach(function(l, i) { l.order = i; });
|
||||||
if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length));
|
if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length));
|
||||||
tabBar.setLayers(layers);
|
tabBar.setLayers(layers);
|
||||||
|
|
@ -1765,8 +1781,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
})();
|
})();
|
||||||
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
|
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
|
||||||
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
|
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
|
||||||
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; const closedMid = layerId.replace('layer-', ''); _closedModuleIds.add(closedMid); tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
||||||
tabBar.addEventListener('layers-close-all', () => { layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
tabBar.addEventListener('layers-close-all', () => { layers.forEach(l => _closedModuleIds.add(l.moduleId)); layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
||||||
tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); });
|
tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue