feat(shell): add Request Access button and Go to My Space fallback on access gate

When a logged-in user visits a private space they don't have access to,
the gate now shows a "Request Access" button (calls POST /api/spaces/:slug/access-requests)
and a "Go to {username}'s Space" secondary link. Handles already-pending (409) gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-02 16:20:56 -07:00
parent 1cdbce0bcc
commit 6364eb8deb
1 changed files with 53 additions and 5 deletions

View File

@ -884,17 +884,20 @@ export function renderShell(opts: ShellOptions): string {
} else { } else {
var userSlug = ''; var userSlug = '';
try { if (session && session.claims && session.claims.username) userSlug = session.claims.username; } catch(e) {} try { if (session && session.claims && session.claims.username) userSlug = session.claims.username; } catch(e) {}
var homeHref = userSlug var mySpaceHref = userSlug
? 'https://' + userSlug + '.rspace.online/' ? 'https://' + userSlug.toLowerCase() + '.rspace.online/'
: 'https://rspace.online/'; : 'https://rspace.online/';
var homeLabel = userSlug ? 'Return to ' + userSlug : 'Return to rSpace'; var mySpaceLabel = userSlug ? 'Go to ' + userSlug + '\\\'s Space' : 'Go to rSpace';
gate.innerHTML = gate.innerHTML =
'<div class="access-gate__card">' + '<div class="access-gate__card">' +
'<div class="access-gate__icon">&#x1F512;</div>' + '<div class="access-gate__icon">&#x1F512;</div>' +
'<h2 class="access-gate__title">Private Space</h2>' + '<h2 class="access-gate__title">Private Space</h2>' +
'<p class="access-gate__desc">You don\\\'t have access to <strong>' + slug + '</strong>.</p>' + '<p class="access-gate__desc">You don\\\'t have access to <strong>' + slug + '</strong>.</p>' +
'<p class="access-gate__hint">This space is private. Ask the owner to invite you as a member.</p>' + '<p class="access-gate__hint">This space is private. You can request access from the owner.</p>' +
'<a href="' + homeHref + '" class="access-gate__btn">' + homeLabel + '</a>' + '<div class="access-gate__actions">' +
'<button class="access-gate__btn" id="gate-request-access">Request Access</button>' +
'<a href="' + mySpaceHref + '" class="access-gate__btn access-gate__btn--secondary">' + mySpaceLabel + '</a>' +
'</div>' +
'</div>'; '</div>';
} }
@ -914,6 +917,44 @@ export function renderShell(opts: ShellOptions): string {
} }
}); });
} }
// Request access button handler
var reqBtn = document.getElementById('gate-request-access');
if (reqBtn) reqBtn.addEventListener('click', function() {
reqBtn.disabled = true;
reqBtn.textContent = 'Requesting...';
fetch('/api/spaces/' + encodeURIComponent(slug) + '/access-requests', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + session.accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({})
})
.then(function(r) { return r.json().then(function(d) { return { status: r.status, data: d }; }); })
.then(function(res) {
if (res.status === 201) {
reqBtn.textContent = 'Request Sent ✓';
reqBtn.style.opacity = '0.7';
var hint = gate.querySelector('.access-gate__hint');
if (hint) hint.textContent = 'Your request has been sent to the space owner. You\\\'ll be notified when it\\\'s approved.';
} else if (res.status === 409) {
reqBtn.textContent = 'Request Pending';
reqBtn.style.opacity = '0.7';
var hint = gate.querySelector('.access-gate__hint');
if (hint) hint.textContent = 'You already have a pending access request for this space.';
} else {
reqBtn.textContent = 'Request Access';
reqBtn.disabled = false;
alert(res.data.error || 'Failed to send request');
}
})
.catch(function() {
reqBtn.textContent = 'Request Access';
reqBtn.disabled = false;
alert('Network error — please try again');
});
});
} }
})(); })();
@ -1755,6 +1796,13 @@ const ACCESS_GATE_CSS = `
transition: opacity 0.15s, transform 0.15s; transition: opacity 0.15s, transform 0.15s;
} }
.access-gate__btn:hover { opacity: 0.9; transform: translateY(-1px); } .access-gate__btn:hover { opacity: 0.9; transform: translateY(-1px); }
.access-gate__btn:disabled { opacity: 0.6; cursor: default; transform: none; }
.access-gate__btn--secondary {
background: transparent; border: 1px solid var(--rs-border, rgba(255,255,255,0.15));
color: var(--rs-text-secondary, #94a3b8);
}
.access-gate__btn--secondary:hover { border-color: var(--rs-border-strong, rgba(255,255,255,0.3)); color: var(--rs-text-primary, #e0e0e0); }
.access-gate__actions { display: flex; flex-direction: column; gap: 0.75rem; align-items: center; }
`; `;
const WELCOME_CSS = ` const WELCOME_CSS = `