fix: enforce subdomain routing — spaces are subdomains, never path segments
Audit and fix ~60 violations across 47 files where URLs were constructed
as /{space}/moduleId instead of /moduleId (subdomain provides the space).
- Add getModuleApiBase() helper to shared/url-helpers.ts
- Fix client components: use getModuleApiBase() for fetch URLs,
rspaceNavUrl() for navigation
- Fix server routes: use c.get("isSubdomain") for redirects and
JSON response URLs
- Fix OAuth callbacks (notion, google, clickup): subdomain-aware redirects
- Fix email notification URLs (rvote): use {space}.rspace.online format
- Fix webhook registration (rtasks/clickup): subdomain-aware endpoint URL
- Replace broken #getSpaceSlug() methods in folk-feed, folk-splat
- Replace NODE_ENV checks with proper isSubdomain checks (rdata, rnetwork)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7c6861bf50
commit
4a54e6af16
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import * as Automerge from "@automerge/automerge";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
import type { CommunitySync } from "./community-sync";
|
||||
import type {
|
||||
CommentPinAnchor,
|
||||
|
|
@ -535,7 +536,7 @@ export class CommentPinManager {
|
|||
if (pin.linkedNoteId) {
|
||||
html += `
|
||||
<div style="padding: 8px 12px; background: #1a1a2e; border-bottom: 1px solid #333; display: flex; align-items: center; justify-content: space-between;">
|
||||
<a href="/${this.#spaceSlug}/rnotes#${pin.linkedNoteId}" target="_blank"
|
||||
<a href="${getModuleApiBase("rnotes")}#${pin.linkedNoteId}" target="_blank"
|
||||
style="color: #a78bfa; text-decoration: none; font-size: 12px; font-weight: 600;">
|
||||
📝 ${this.#escapeHtml(pin.linkedNoteTitle || "Linked note")}
|
||||
</a>
|
||||
|
|
@ -669,7 +670,7 @@ export class CommentPinManager {
|
|||
if (this.#members) return this.#members;
|
||||
try {
|
||||
const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
|
||||
const res = await fetch(`/${this.#spaceSlug}/api/space-members`, {
|
||||
const res = await fetch(`${getModuleApiBase("rspace")}/api/space-members`, {
|
||||
headers: sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {},
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
|
|
@ -685,7 +686,7 @@ export class CommentPinManager {
|
|||
if (this.#notes) return this.#notes;
|
||||
try {
|
||||
const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
|
||||
const res = await fetch(`/${this.#spaceSlug}/rnotes/api/notes?limit=50`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/notes?limit=50`, {
|
||||
headers: sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {},
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
|
|
@ -816,7 +817,7 @@ export class CommentPinManager {
|
|||
|
||||
try {
|
||||
const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
|
||||
await fetch(`/${this.#spaceSlug}/api/comment-pins/notify-mention`, {
|
||||
await fetch(`${getModuleApiBase("rspace")}/api/comment-pins/notify-mention`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -879,7 +880,7 @@ export class CommentPinManager {
|
|||
};
|
||||
if (email) body.notifyEmail = email;
|
||||
|
||||
const res = await fetch(`/${this.#spaceSlug}/rschedule/api/reminders`, {
|
||||
const res = await fetch(`${getModuleApiBase("rschedule")}/api/reminders`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
// ── Skill constants (mirrored from rtime/schemas to avoid server import) ──
|
||||
|
||||
|
|
@ -299,7 +300,7 @@ export class FolkCommitmentPool extends FolkShape {
|
|||
|
||||
async #fetchCommitments() {
|
||||
try {
|
||||
const resp = await fetch(`/${this.#spaceSlug}/rtime/api/commitments`);
|
||||
const resp = await fetch(`${getModuleApiBase("rtime")}/api/commitments`);
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const commitments: PoolCommitment[] = data.commitments || [];
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { FLOW_COLORS, FLOW_LABELS } from "./layer-types";
|
||||
import type { FlowKind } from "./layer-types";
|
||||
import { getModuleApiBase, rspaceNavUrl, getCurrentSpace } from "../shared/url-helpers";
|
||||
|
||||
export class FolkFeed extends FolkShape {
|
||||
static tagName = "folk-feed";
|
||||
|
|
@ -121,8 +122,7 @@ export class FolkFeed extends FolkShape {
|
|||
this.#inner.querySelector(".feed-navigate")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.sourceModule) {
|
||||
const space = this.#getSpaceSlug();
|
||||
window.location.href = `/${space}/${this.sourceModule}`;
|
||||
window.location.href = rspaceNavUrl(getCurrentSpace(), this.sourceModule);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -150,9 +150,8 @@ export class FolkFeed extends FolkShape {
|
|||
|
||||
try {
|
||||
// Construct feed URL based on feed ID
|
||||
const space = this.#getSpaceSlug();
|
||||
const feedEndpoint = this.#getFeedEndpoint();
|
||||
const url = `/${space}/${this.sourceModule}/api/${feedEndpoint}`;
|
||||
const url = `${getModuleApiBase(this.sourceModule)}/api/${feedEndpoint}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
|
@ -257,12 +256,6 @@ export class FolkFeed extends FolkShape {
|
|||
return moduleEndpoints[this.feedId] || moduleEndpoints.default || this.feedId;
|
||||
}
|
||||
|
||||
#getSpaceSlug(): string {
|
||||
// Try to get from URL
|
||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||
return parts[0] || "demo";
|
||||
}
|
||||
|
||||
// ── Render feed items ──
|
||||
|
||||
#renderItems() {
|
||||
|
|
@ -327,7 +320,6 @@ export class FolkFeed extends FolkShape {
|
|||
|
||||
/** Navigate to the source item in its module */
|
||||
#navigateToItem(item: any) {
|
||||
const space = this.#getSpaceSlug();
|
||||
const mod = this.sourceModule;
|
||||
|
||||
// Build a deep link to the item in its source module
|
||||
|
|
@ -338,7 +330,7 @@ export class FolkFeed extends FolkShape {
|
|||
sourceModule: mod,
|
||||
itemId: item.id,
|
||||
item,
|
||||
url: `/${space}/${mod}`,
|
||||
url: getModuleApiBase(mod),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
@ -427,9 +419,8 @@ export class FolkFeed extends FolkShape {
|
|||
|
||||
// Write back to source module API
|
||||
try {
|
||||
const space = this.#getSpaceSlug();
|
||||
const endpoint = this.#getWriteBackEndpoint(item);
|
||||
const url = `/${space}/${this.sourceModule}/api/${endpoint}`;
|
||||
const url = `${getModuleApiBase(this.sourceModule)}/api/${endpoint}`;
|
||||
|
||||
const method = this.#getWriteBackMethod();
|
||||
const token = this.#getAuthToken();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
import type {
|
||||
RoomState,
|
||||
ParticipantState,
|
||||
|
|
@ -942,9 +943,7 @@ export class FolkMap extends FolkShape {
|
|||
}
|
||||
|
||||
#getApiBase(): string {
|
||||
const match = window.location.pathname.match(/^\/([^/]+)/);
|
||||
const spaceSlug = match ? match[1] : "default";
|
||||
return `/${spaceSlug}/rmaps`;
|
||||
return getModuleApiBase("rmaps");
|
||||
}
|
||||
|
||||
async #initRoomSync() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
|
|
@ -295,8 +296,7 @@ export class FolkMultisigEmail extends FolkShape {
|
|||
}
|
||||
|
||||
private getApiBase(): string {
|
||||
const space = (this as any).spaceSlug || (window as any).__communitySync?.communitySlug || "demo";
|
||||
return `/${space}/rinbox`;
|
||||
return getModuleApiBase("rinbox");
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
const PLATFORM_ICONS: Record<string, string> = {
|
||||
x: "𝕏", linkedin: "in", instagram: "📷", youtube: "▶",
|
||||
|
|
@ -331,7 +332,7 @@ export class FolkSocialCampaign extends FolkShape {
|
|||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("navigate-to-module", {
|
||||
bubbles: true, composed: true,
|
||||
detail: { path: `/${this.#spaceSlug}/rsocials/campaigns?id=${this.#campaignId}` },
|
||||
detail: { path: `${getModuleApiBase("rsocials")}/campaigns?id=${this.#campaignId}` },
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import type { SocialPlatform } from "./folk-social-post";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
const PLATFORM_COLORS: Record<string, string> = {
|
||||
x: "#000000",
|
||||
|
|
@ -331,7 +332,7 @@ export class FolkSocialThread extends FolkShape {
|
|||
this.dispatchEvent(new CustomEvent("navigate-to-module", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { path: `/${this.#spaceSlug}/rsocials/thread-editor?id=${this.#threadId}` },
|
||||
detail: { path: `${getModuleApiBase("rsocials")}/thread-editor?id=${this.#threadId}` },
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
import {
|
||||
computeSpider3D,
|
||||
computeRadarPolygon,
|
||||
|
|
@ -488,7 +489,7 @@ export class FolkSpider3D extends FolkShape {
|
|||
|
||||
try {
|
||||
const token = localStorage.getItem("rspace-token") || "";
|
||||
const res = await fetch(`/${this.#space}/connections`, {
|
||||
const res = await fetch(`${getModuleApiBase("rspace")}/connections`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
|
|
@ -327,8 +328,7 @@ export class FolkSplat extends FolkShape {
|
|||
async #loadGallery(container: HTMLElement, viewerArea: HTMLElement) {
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div><span>Loading gallery...</span></div>';
|
||||
try {
|
||||
const spaceSlug = this.#getSpaceSlug();
|
||||
const res = await fetch(`/${spaceSlug}/rsplat/api/splats?limit=20`);
|
||||
const res = await fetch(`${getModuleApiBase("rsplat")}/api/splats?limit=20`);
|
||||
if (!res.ok) throw new Error("Failed to load splats");
|
||||
const data = await res.json();
|
||||
this.#gallerySplats = data.splats || [];
|
||||
|
|
@ -351,8 +351,7 @@ export class FolkSplat extends FolkShape {
|
|||
const slug = (item as HTMLElement).dataset.slug;
|
||||
const splat = this.#gallerySplats.find((s) => s.slug === slug);
|
||||
if (splat) {
|
||||
const spaceSlug = this.#getSpaceSlug();
|
||||
const url = `/${spaceSlug}/rsplat/api/splats/${splat.slug}/${splat.slug}.${splat.file_format}`;
|
||||
const url = `${getModuleApiBase("rsplat")}/api/splats/${splat.slug}/${splat.slug}.${splat.file_format}`;
|
||||
if (this.#urlInput) this.#urlInput.value = url;
|
||||
container.style.display = "none";
|
||||
this.#loadSplat(url, viewerArea);
|
||||
|
|
@ -410,19 +409,13 @@ export class FolkSplat extends FolkShape {
|
|||
} catch (err) {
|
||||
// Fallback: show as iframe pointing to splat viewer page
|
||||
console.warn("[folk-splat] Three.js not available, using iframe fallback");
|
||||
const spaceSlug = this.#getSpaceSlug();
|
||||
const slug = url.split("/").filter(Boolean).pop()?.split(".")[0] || "";
|
||||
viewerArea.innerHTML = `<iframe src="/${spaceSlug}/rsplat/view/${slug}" style="width:100%;height:100%;border:none;border-radius:0 0 8px 8px"></iframe>`;
|
||||
viewerArea.innerHTML = `<iframe src="${getModuleApiBase("rsplat")}/view/${slug}" style="width:100%;height:100%;border:none;border-radius:0 0 8px 8px"></iframe>`;
|
||||
} finally {
|
||||
this.#isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
#getSpaceSlug(): string {
|
||||
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
||||
return pathParts[0] || "demo";
|
||||
}
|
||||
|
||||
#escapeHtml(text: string): string {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
// Skill constants (mirrored from rtime/schemas)
|
||||
const SKILL_COLORS: Record<string, string> = {
|
||||
|
|
@ -404,7 +405,7 @@ export class FolkTaskRequest extends FolkShape {
|
|||
// Get auth token from cookie or localStorage
|
||||
const token = document.cookie.split(";").map(c => c.trim()).find(c => c.startsWith("auth_token="))?.split("=")[1]
|
||||
|| localStorage.getItem("auth_token") || "";
|
||||
await fetch(`/${this.#spaceSlug}/rtime/api/connections`, {
|
||||
await fetch(`${getModuleApiBase("rtime")}/api/connections`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
import { getModuleApiBase } from "../shared/url-helpers";
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
|
|
@ -283,8 +284,7 @@ export class FolkTransactionBuilder extends FolkShape {
|
|||
}
|
||||
|
||||
#apiBase(): string {
|
||||
const space = this.#getSpaceSlug();
|
||||
return `/${space}/rwallet/api/safe/${this.#chainId}/${this.#safeAddress}`;
|
||||
return `${getModuleApiBase("rwallet")}/api/safe/${this.#chainId}/${this.#safeAddress}`;
|
||||
}
|
||||
|
||||
async #apiFetch(path: string, opts?: RequestInit): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
|||
import { CrowdSurfLocalFirstClient } from '../local-first-client';
|
||||
import type { CrowdSurfDoc, CrowdSurfPrompt, Contribution } from '../schemas';
|
||||
import { getDecayProgress, getTimeRemaining, getRightSwipeCount, isReadyToTrigger, getUrgency, parseContributions } from '../schemas';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
// ── Auth helpers ──
|
||||
function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null {
|
||||
|
|
@ -624,7 +625,7 @@ class FolkCrowdSurfDashboard extends HTMLElement {
|
|||
this.rankLoading = true;
|
||||
this.render();
|
||||
this.bindEvents();
|
||||
fetch(`/${this.space}/crowdsurf/api/crowdsurf/pair?space=${this.space}`)
|
||||
fetch(`${getModuleApiBase("crowdsurf")}/api/crowdsurf/pair?space=${this.space}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.a && data.b) {
|
||||
|
|
@ -697,7 +698,7 @@ class FolkCrowdSurfDashboard extends HTMLElement {
|
|||
}
|
||||
|
||||
// Live mode: POST to API
|
||||
fetch(`/${this.space}/crowdsurf/api/crowdsurf/compare?space=${this.space}`, {
|
||||
fetch(`${getModuleApiBase("crowdsurf")}/api/crowdsurf/compare?space=${this.space}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ winnerId, loserId }),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { TourStep } from '../../../shared/tour-engine';
|
|||
import './folk-listing';
|
||||
import './folk-stay-request';
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
const BNB_TOUR_STEPS: TourStep[] = [
|
||||
{ target: '.bnb-search', title: 'Search', message: 'Filter listings by location, type, or economy model.' },
|
||||
|
|
@ -89,9 +90,9 @@ class FolkBnbView extends HTMLElement {
|
|||
async #loadData() {
|
||||
try {
|
||||
const [listingsRes, staysRes, statsRes] = await Promise.all([
|
||||
fetch(`/${this.#space}/rbnb/api/listings`),
|
||||
fetch(`/${this.#space}/rbnb/api/stays`),
|
||||
fetch(`/${this.#space}/rbnb/api/stats`),
|
||||
fetch(`${getModuleApiBase("rbnb")}/api/listings`),
|
||||
fetch(`${getModuleApiBase("rbnb")}/api/stays`),
|
||||
fetch(`${getModuleApiBase("rbnb")}/api/stats`),
|
||||
]);
|
||||
|
||||
if (listingsRes.ok) {
|
||||
|
|
@ -414,7 +415,7 @@ class FolkBnbView extends HTMLElement {
|
|||
|
||||
async #handleStayAction(stayId: string, action: string) {
|
||||
try {
|
||||
const res = await fetch(`/${this.#space}/rbnb/api/stays/${stayId}/${action}`, {
|
||||
const res = await fetch(`${getModuleApiBase("rbnb")}/api/stays/${stayId}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
|
@ -430,7 +431,7 @@ class FolkBnbView extends HTMLElement {
|
|||
|
||||
async #handleStayMessage(stayId: string, body: string) {
|
||||
try {
|
||||
const res = await fetch(`/${this.#space}/rbnb/api/stays/${stayId}/messages`, {
|
||||
const res = await fetch(`${getModuleApiBase("rbnb")}/api/stays/${stayId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ body, sender_name: 'You' }),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
* capacity, location, economy badge, availability dot, endorsement count.
|
||||
*/
|
||||
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
const ECONOMY_COLORS: Record<string, { bg: string; fg: string; label: string; icon: string }> = {
|
||||
gift: { bg: 'rgba(52,211,153,0.12)', fg: '#34d399', label: 'Gift', icon: '\u{1F49A}' },
|
||||
exchange: { bg: 'rgba(96,165,250,0.12)', fg: '#60a5fa', label: 'Exchange', icon: '\u{1F91D}' },
|
||||
|
|
@ -53,7 +55,7 @@ class FolkListing extends HTMLElement {
|
|||
if (!listingId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${space}/rbnb/api/listings/${listingId}`);
|
||||
const res = await fetch(`${getModuleApiBase("rbnb")}/api/listings/${listingId}`);
|
||||
if (res.ok) {
|
||||
this.#data = await res.json();
|
||||
this.#render();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
* action buttons (accept/decline/cancel/complete), endorsement prompt.
|
||||
*/
|
||||
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
const STATUS_STYLES: Record<string, { bg: string; fg: string; label: string }> = {
|
||||
pending: { bg: 'rgba(245,158,11,0.15)', fg: '#f59e0b', label: 'Pending' },
|
||||
accepted: { bg: 'rgba(52,211,153,0.15)', fg: '#34d399', label: 'Accepted' },
|
||||
|
|
@ -50,7 +52,7 @@ class FolkStayRequest extends HTMLElement {
|
|||
if (!stayId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${space}/rbnb/api/stays/${stayId}`);
|
||||
const res = await fetch(`${getModuleApiBase("rbnb")}/api/stays/${stayId}`);
|
||||
if (res.ok) {
|
||||
this.#data = await res.json();
|
||||
this.#render();
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ routes.get("/read/:id", async (c) => {
|
|||
});
|
||||
|
||||
// Build the PDF URL relative to this module's mount point
|
||||
const pdfUrl = `/${spaceSlug}/rbooks/api/books/${book.slug}/pdf`;
|
||||
const pdfUrl = c.get("isSubdomain") ? `/rbooks/api/books/${book.slug}/pdf` : `/${spaceSlug}/rbooks/api/books/${book.slug}/pdf`;
|
||||
|
||||
const html = renderShell({
|
||||
title: `${book.title} | rSpace`,
|
||||
|
|
|
|||
|
|
@ -633,7 +633,7 @@ class FolkCartShop extends HTMLElement {
|
|||
this.shadow.querySelectorAll("[data-action='copy-pay-url']").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const payId = (el as HTMLElement).dataset.payId;
|
||||
const url = `${window.location.origin}/${this.space}/rcart/pay/${payId}`;
|
||||
const url = `${window.location.origin}${this.getApiBase()}/pay/${payId}`;
|
||||
navigator.clipboard.writeText(url);
|
||||
(el as HTMLElement).textContent = 'Copied!';
|
||||
setTimeout(() => { (el as HTMLElement).textContent = 'Copy Link'; }, 2000);
|
||||
|
|
@ -644,7 +644,7 @@ class FolkCartShop extends HTMLElement {
|
|||
this.shadow.querySelectorAll("[data-group-buy-id]").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const buyId = (el as HTMLElement).dataset.groupBuyId!;
|
||||
const url = `/${this.space}/rcart/buy/${buyId}`;
|
||||
const url = `${this.getApiBase()}/buy/${buyId}`;
|
||||
window.location.href = url;
|
||||
});
|
||||
});
|
||||
|
|
@ -1116,7 +1116,7 @@ class FolkCartShop extends HTMLElement {
|
|||
if (this.groupBuys.length === 0) {
|
||||
return `<div class="empty">
|
||||
<p>No group buys yet. Start one from any catalog item to unlock volume pricing together.</p>
|
||||
<a class="btn btn-primary" href="/${this.space}/rcart/catalog">Browse Catalog</a>
|
||||
<a class="btn btn-primary" href="${this.getApiBase()}/catalog">Browse Catalog</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1333,7 +1333,7 @@ class FolkCartShop extends HTMLElement {
|
|||
<div class="card-meta">${new Date(pay.created_at).toLocaleDateString()}</div>
|
||||
${pay.status === 'pending' ? `
|
||||
<div style="margin-top:0.75rem; display:flex; gap:0.5rem">
|
||||
<a class="btn btn-sm" href="/${this.space}/rcart/pay/${pay.id}" target="_blank" rel="noopener">Open</a>
|
||||
<a class="btn btn-sm" href="${this.getApiBase()}/pay/${pay.id}" target="_blank" rel="noopener">Open</a>
|
||||
<button class="btn btn-sm" data-action="copy-pay-url" data-pay-id="${pay.id}">Copy Link</button>
|
||||
<a class="btn btn-sm" href="${this.getApiBase()}/api/payments/${pay.id}/qr" target="_blank" rel="noopener">QR</a>
|
||||
</div>` : ''}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class FolkPaymentPage extends HTMLElement {
|
|||
disconnectedCallback() {
|
||||
this.stopPolling();
|
||||
this.walletDiscovery?.stop?.();
|
||||
window.removeEventListener('message', this.handleTransakMessage);
|
||||
window.removeEventListener('message', this.handlePaymentMessage);
|
||||
}
|
||||
|
||||
private getApiBase(): string {
|
||||
|
|
@ -116,7 +116,7 @@ class FolkPaymentPage extends HTMLElement {
|
|||
private async generateQR() {
|
||||
try {
|
||||
const QRCode = await import('qrcode');
|
||||
const payUrl = `${window.location.origin}/${this.space}/rcart/pay/${this.paymentId}`;
|
||||
const payUrl = `${window.location.origin}${this.getApiBase()}/pay/${this.paymentId}`;
|
||||
this.qrDataUrl = await QRCode.toDataURL(payUrl, { margin: 2, width: 200 });
|
||||
} catch { /* QR generation optional */ }
|
||||
}
|
||||
|
|
@ -528,7 +528,7 @@ class FolkPaymentPage extends HTMLElement {
|
|||
const explorer = explorerBase[p.chainId] || '';
|
||||
|
||||
const cartLink = p.linkedCartId
|
||||
? `<div style="margin-top:1.25rem"><a href="/${this.space}/rcart/carts" class="btn btn-primary" style="display:inline-block; text-decoration:none; text-align:center;">Return to Cart</a></div>`
|
||||
? `<div style="margin-top:1.25rem"><a href="${this.getApiBase()}/carts" class="btn btn-primary" style="display:inline-block; text-decoration:none; text-align:center;">Return to Cart</a></div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
|
|
|
|||
|
|
@ -155,8 +155,8 @@ class FolkPaymentRequest extends HTMLElement {
|
|||
|
||||
// Build URLs
|
||||
const host = window.location.origin;
|
||||
this.payUrl = `${host}/${this.space}/rcart/pay/${paymentId}`;
|
||||
this.qrSvgUrl = `${host}/${this.space}/rcart/api/payments/${paymentId}/qr`;
|
||||
this.payUrl = `${host}${this.getApiBase()}/pay/${paymentId}`;
|
||||
this.qrSvgUrl = `${host}${this.getApiBase()}/api/payments/${paymentId}/qr`;
|
||||
|
||||
// Generate client-side QR
|
||||
try {
|
||||
|
|
@ -296,8 +296,8 @@ class FolkPaymentRequest extends HTMLElement {
|
|||
|
||||
// Build URLs
|
||||
const host = window.location.origin;
|
||||
this.payUrl = `${host}/${this.space}/rcart/pay/${this.generatedPayment.id}`;
|
||||
this.qrSvgUrl = `${host}/${this.space}/rcart/api/payments/${this.generatedPayment.id}/qr`;
|
||||
this.payUrl = `${host}${this.getApiBase()}/pay/${this.generatedPayment.id}`;
|
||||
this.qrSvgUrl = `${host}${this.getApiBase()}/api/payments/${this.generatedPayment.id}/qr`;
|
||||
|
||||
// Generate client-side QR
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { TourEngine } from "../../../shared/tour-engine";
|
|||
import { ChoicesLocalFirstClient } from "../local-first-client";
|
||||
import type { ChoicesDoc, ChoiceSession, ChoiceVote } from "../schemas";
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
import { getModuleApiBase, rspaceNavUrl, getCurrentSpace } from "../../../shared/url-helpers";
|
||||
|
||||
// ── CrowdSurf types ──
|
||||
interface CrowdSurfOption {
|
||||
|
|
@ -298,7 +299,7 @@ class FolkChoicesDashboard extends HTMLElement {
|
|||
${isLive ? `<span class="live-badge"><span class="live-dot"></span>LIVE</span>` : ''}
|
||||
<div class="create-btns">
|
||||
${this.lfClient ? `<button class="create-btn" data-action="new-session">+ New Poll</button>` : ''}
|
||||
<a class="create-btn" href="/${this.space}/rspace" title="Open canvas to create choices">+ Canvas</a>
|
||||
<a class="create-btn" href="${rspaceNavUrl(this.space, "rspace")}" title="Open canvas to create choices">+ Canvas</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -472,14 +473,14 @@ class FolkChoicesDashboard extends HTMLElement {
|
|||
return `<div class="empty">
|
||||
<div class="empty-icon">☑</div>
|
||||
<p>No choices in this space yet.</p>
|
||||
<p>Open the <a href="/${this.space}/rspace" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
|
||||
<p>Open the <a href="${rspaceNavUrl(this.space, "rspace")}" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderGrid(icons: Record<string, string>, labels: Record<string, string>): string {
|
||||
return `<div class="grid">
|
||||
${this.choices.map((ch) => `
|
||||
<a class="card" data-collab-id="choice:${ch.id}" href="/${this.space}/rspace">
|
||||
<a class="card" data-collab-id="choice:${ch.id}" href="${rspaceNavUrl(this.space, "rspace")}">
|
||||
<div class="card-icon">${icons[ch.type] || "☑"}</div>
|
||||
<div class="card-type">${labels[ch.type] || ch.type}</div>
|
||||
<h3 class="card-title">${this.esc(ch.title)}</h3>
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ const DATA_TABS = [
|
|||
|
||||
const DATA_TAB_IDS = new Set(DATA_TABS.map((t) => t.id));
|
||||
|
||||
function renderDataPage(space: string, activeTab: string) {
|
||||
function renderDataPage(space: string, activeTab: string, isSubdomain: boolean) {
|
||||
const isTree = activeTab === "tree";
|
||||
const body = isTree
|
||||
? `<folk-content-tree space="${space}"></folk-content-tree>`
|
||||
|
|
@ -266,14 +266,14 @@ function renderDataPage(space: string, activeTab: string) {
|
|||
styles: `<link rel="stylesheet" href="/modules/rdata/data.css">`,
|
||||
tabs: [...DATA_TABS],
|
||||
activeTab,
|
||||
tabBasePath: process.env.NODE_ENV === "production" ? `/rdata` : `/${space}/rdata`,
|
||||
tabBasePath: isSubdomain ? `/rdata` : `/${space}/rdata`,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Page routes ──
|
||||
routes.get("/", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderDataPage(space, "tree"));
|
||||
return c.html(renderDataPage(space, "tree", c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
routes.get("/:tabId", (c, next) => {
|
||||
|
|
@ -282,9 +282,9 @@ routes.get("/:tabId", (c, next) => {
|
|||
// Skip API and asset routes — let Hono fall through
|
||||
if (tabId.startsWith("api") || tabId.includes(".")) return next();
|
||||
if (!DATA_TAB_IDS.has(tabId as any)) {
|
||||
return c.redirect(process.env.NODE_ENV === "production" ? `/rdata` : `/${space}/rdata`, 302);
|
||||
return c.redirect(c.get("isSubdomain") ? `/rdata` : `/${space}/rdata`, 302);
|
||||
}
|
||||
return c.html(renderDataPage(space, tabId));
|
||||
return c.html(renderDataPage(space, tabId, c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
export const dataModule: RSpaceModule = {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { TourEngine } from "../../../shared/tour-engine";
|
|||
import { ViewHistory } from "../../../shared/view-history.js";
|
||||
import { getAccessToken, getUsername } from "../../../lib/rspace-header";
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Link from '@tiptap/extension-link';
|
||||
|
|
@ -1694,7 +1695,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
</div>
|
||||
|
||||
<div style="text-align:center;margin-top:1.5rem">
|
||||
<a href="/${this.space}/rinbox/about" class="help-cta">View Full Feature Page →</a>
|
||||
<a href="${getModuleApiBase("rinbox")}/about" class="help-cta">View Full Feature Page →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { RoomSync, type RoomState, type ParticipantState, type LocationState, ty
|
|||
import { loadRoomHistory, saveRoomVisit, updateRoomThumbnail } from "./map-room-history";
|
||||
import { MapPushManager } from "./map-push";
|
||||
import { fuzzLocation, haversineDistance, formatDistance, formatTime } from "./map-privacy";
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
import "./map-privacy-panel";
|
||||
import { TourEngine } from "../../../shared/tour-engine";
|
||||
import { ViewHistory } from "../../../shared/view-history.js";
|
||||
|
|
@ -1687,7 +1688,7 @@ class FolkMapViewer extends HTMLElement {
|
|||
import("./map-share-modal");
|
||||
const modal = document.createElement("map-share-modal") as any;
|
||||
modal.id = "share-modal";
|
||||
modal.url = `${window.location.origin}/${this.space}/rmaps/${this.room}`;
|
||||
modal.url = `${window.location.origin}${getModuleApiBase("rmaps")}/${this.room}`;
|
||||
modal.room = this.room;
|
||||
modal.addEventListener("modal-close", () => { this.showShareModal = false; });
|
||||
this.shadow.appendChild(modal);
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ routes.get("/recordings", async (c) => {
|
|||
routes.get("/recordings/:id", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
const id = c.req.param("id");
|
||||
return c.redirect(`/${space}/rmeets/recordings/${id}/overview`);
|
||||
return c.redirect(c.get("isSubdomain") ? `/rmeets/recordings/${id}/overview` : `/${space}/rmeets/recordings/${id}/overview`);
|
||||
});
|
||||
|
||||
// ── Recording detail with tabs ──
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@ const CRM_TABS = [
|
|||
|
||||
const CRM_TAB_IDS = new Set(CRM_TABS.map(t => t.id));
|
||||
|
||||
function renderCrm(space: string, activeTab: string) {
|
||||
function renderCrm(space: string, activeTab: string, isSubdomain: boolean) {
|
||||
return renderShell({
|
||||
title: `${space} — CRM | rSpace`,
|
||||
moduleId: "rnetwork",
|
||||
|
|
@ -698,13 +698,13 @@ function renderCrm(space: string, activeTab: string) {
|
|||
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
|
||||
tabs: [...CRM_TABS],
|
||||
activeTab,
|
||||
tabBasePath: process.env.NODE_ENV === "production" ? `/rnetwork/crm` : `/${space}/rnetwork/crm`,
|
||||
tabBasePath: isSubdomain ? `/rnetwork/crm` : `/${space}/rnetwork/crm`,
|
||||
});
|
||||
}
|
||||
|
||||
routes.get("/crm", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderCrm(space, "pipeline"));
|
||||
return c.html(renderCrm(space, "pipeline", c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
// Tab subpath routes: /crm/:tabId
|
||||
|
|
@ -712,9 +712,9 @@ routes.get("/crm/:tabId", (c) => {
|
|||
const space = c.req.param("space") || "demo";
|
||||
const tabId = c.req.param("tabId");
|
||||
if (!CRM_TAB_IDS.has(tabId as any)) {
|
||||
return c.redirect(`/${space}/rnetwork/crm`, 302);
|
||||
return c.redirect(c.get("isSubdomain") ? `/rnetwork/crm` : `/${space}/rnetwork/crm`, 302);
|
||||
}
|
||||
return c.html(renderCrm(space, tabId));
|
||||
return c.html(renderCrm(space, tabId, c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
// ── Page route ──
|
||||
|
|
@ -723,7 +723,7 @@ routes.get("/", (c) => {
|
|||
const view = c.req.query("view");
|
||||
|
||||
if (view === "app") {
|
||||
return c.redirect(`/${space}/rnetwork/crm`, 301);
|
||||
return c.redirect(c.get("isSubdomain") ? `/rnetwork/crm` : `/${space}/rnetwork/crm`, 301);
|
||||
}
|
||||
|
||||
return c.html(renderShell({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import type { DocumentId } from '../../../shared/local-first/document';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
interface CommentMessage {
|
||||
id: string;
|
||||
|
|
@ -731,7 +732,7 @@ class NotesCommentPanel extends HTMLElement {
|
|||
// Try creating a reminder via rSchedule API (non-demo only)
|
||||
if (!this.isDemo && this._space) {
|
||||
try {
|
||||
const res = await fetch(`/${this._space}/rschedule/api/reminders`, {
|
||||
const res = await fetch(`${getModuleApiBase("rschedule")}/api/reminders`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
|
||||
body: JSON.stringify({
|
||||
|
|
@ -785,7 +786,7 @@ class NotesCommentPanel extends HTMLElement {
|
|||
// Delete from rSchedule if exists
|
||||
if (reminderId && !this.isDemo && this._space) {
|
||||
try {
|
||||
await fetch(`/${this._space}/rschedule/api/reminders/${reminderId}`, {
|
||||
await fetch(`${getModuleApiBase("rschedule")}/api/reminders/${reminderId}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.authHeaders(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
* API-based sources (Notion/Google) use OAuth connections.
|
||||
*/
|
||||
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
interface NotebookOption {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -84,7 +86,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
|
||||
private async loadConnections() {
|
||||
try {
|
||||
const res = await fetch(`/${this.space}/rnotes/api/connections`);
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/connections`);
|
||||
if (res.ok) {
|
||||
this.connections = await res.json();
|
||||
}
|
||||
|
|
@ -97,7 +99,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
|
||||
if (this.activeSource === 'notion') {
|
||||
try {
|
||||
const res = await fetch(`/${this.space}/rnotes/api/import/notion/pages`);
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/import/notion/pages`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.remotePages = data.pages || [];
|
||||
|
|
@ -105,7 +107,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
} catch { /* ignore */ }
|
||||
} else if (this.activeSource === 'google-docs') {
|
||||
try {
|
||||
const res = await fetch(`/${this.space}/rnotes/api/import/google-docs/list`);
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/import/google-docs/list`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.remotePages = data.docs || [];
|
||||
|
|
@ -145,7 +147,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
if (this.targetNotebookId) formData.append('notebookId', this.targetNotebookId);
|
||||
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/import/files`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/import/files`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: formData,
|
||||
|
|
@ -177,7 +179,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
}
|
||||
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/import/upload`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/import/upload`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: formData,
|
||||
|
|
@ -201,7 +203,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
}
|
||||
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/import/notion`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/import/notion`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
|
@ -228,7 +230,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
}
|
||||
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/import/google-docs`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/import/google-docs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
|
@ -267,7 +269,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
try {
|
||||
if (this.activeSource === 'obsidian' || this.activeSource === 'logseq' || this.activeSource === 'markdown' as any) {
|
||||
const format = this.activeSource === 'markdown' as any ? 'markdown' : this.activeSource;
|
||||
const url = `/${this.space}/rnotes/api/export/${format}?notebookId=${encodeURIComponent(this.targetNotebookId)}`;
|
||||
const url = `${getModuleApiBase("rnotes")}/api/export/${format}?notebookId=${encodeURIComponent(this.targetNotebookId)}`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
|
|
@ -288,7 +290,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
}
|
||||
} else if (this.activeSource === 'notion') {
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/export/notion`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/export/notion`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
|
@ -305,7 +307,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
}
|
||||
} else if (this.activeSource === 'google-docs') {
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/export/google-docs`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/export/google-docs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
|
@ -562,7 +564,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
|
||||
private async loadSyncStatus(notebookId: string) {
|
||||
try {
|
||||
const res = await fetch(`/${this.space}/rnotes/api/sync/status/${notebookId}`);
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/sync/status/${notebookId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.syncStatuses = data.statuses || {};
|
||||
|
|
@ -579,7 +581,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
|
||||
try {
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/sync/notebook/${this.targetNotebookId}`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/sync/notebook/${this.targetNotebookId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
|
|
@ -624,7 +626,7 @@ class ImportExportDialog extends HTMLElement {
|
|||
formData.append('source', source);
|
||||
|
||||
const token = localStorage.getItem('encryptid_token') || '';
|
||||
const res = await fetch(`/${this.space}/rnotes/api/sync/upload`, {
|
||||
const res = await fetch(`${getModuleApiBase("rnotes")}/api/sync/upload`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: formData,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { pubsDraftSchema, pubsDocId } from '../schemas';
|
|||
import type { PubsDoc } from '../schemas';
|
||||
import type { DocumentId } from '../../../shared/local-first/document';
|
||||
import { TourEngine } from '../../../shared/tour-engine';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
|
||||
interface BookFormat {
|
||||
|
|
@ -445,7 +446,7 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
<input type="file" accept=".md,.txt,.markdown" style="display:none" />
|
||||
Open File
|
||||
</label>
|
||||
<a class="toolbar-btn btn-zine-gen" href="/${this._spaceSlug}/rpubs/zine" title="AI Zine Generator">
|
||||
<a class="toolbar-btn btn-zine-gen" href="${getModuleApiBase("rpubs")}/zine" title="AI Zine Generator">
|
||||
📰 Zine
|
||||
</a>
|
||||
<button class="toolbar-btn btn-tour-trigger" title="Take a tour" style="opacity:0.7">Tour</button>
|
||||
|
|
@ -766,7 +767,7 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
const s = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
|
||||
if (s?.accessToken) authHeaders["Authorization"] = `Bearer ${s.accessToken}`;
|
||||
} catch {}
|
||||
const res = await fetch(`/${this._spaceSlug}/rpubs/api/generate`, {
|
||||
const res = await fetch(`${getModuleApiBase("rpubs")}/api/generate`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...authHeaders },
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
* space-slug — Current space slug for API calls
|
||||
*/
|
||||
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
export class FolkPubsPublishPanel extends HTMLElement {
|
||||
private _pdfUrl = "";
|
||||
private _formatId = "";
|
||||
|
|
@ -373,7 +375,7 @@ export class FolkPubsPublishPanel extends HTMLElement {
|
|||
this.render();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${this._spaceSlug}/rpubs/api/email-pdf`, {
|
||||
const res = await fetch(`${getModuleApiBase("rpubs")}/api/email-pdf`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
|
@ -403,7 +405,7 @@ export class FolkPubsPublishPanel extends HTMLElement {
|
|||
this.render();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${this._spaceSlug}/rpubs/api/imposition`, {
|
||||
const res = await fetch(`${getModuleApiBase("rpubs")}/api/imposition`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
|
@ -517,7 +519,7 @@ export class FolkPubsPublishPanel extends HTMLElement {
|
|||
|
||||
const { latitude: lat, longitude: lng } = pos.coords;
|
||||
const res = await fetch(
|
||||
`/${this._spaceSlug}/rpubs/api/printers?lat=${lat}&lng=${lng}&radius=100&format=${this._formatId}`,
|
||||
`${getModuleApiBase("rpubs")}/api/printers?lat=${lat}&lng=${lng}&radius=100&format=${this._formatId}`,
|
||||
);
|
||||
|
||||
if (!res.ok) throw new Error("Failed to search printers");
|
||||
|
|
@ -538,7 +540,7 @@ export class FolkPubsPublishPanel extends HTMLElement {
|
|||
private async placeOrder() {
|
||||
if (!this._selectedProvider) return;
|
||||
try {
|
||||
const res = await fetch(`/${this._spaceSlug}/rpubs/api/order`, {
|
||||
const res = await fetch(`${getModuleApiBase("rpubs")}/api/order`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
|
@ -569,7 +571,7 @@ export class FolkPubsPublishPanel extends HTMLElement {
|
|||
private async joinBatch() {
|
||||
if (!this._selectedProvider) return;
|
||||
try {
|
||||
const res = await fetch(`/${this._spaceSlug}/rpubs/api/batch`, {
|
||||
const res = await fetch(`${getModuleApiBase("rpubs")}/api/batch`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -629,7 +629,7 @@ export class FolkCampaignManager extends HTMLElement {
|
|||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${this._space}/rsocials/api/campaign/generate`, {
|
||||
const res = await fetch(`${this.basePath}api/campaign/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ brief, platforms, tone, style }),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
*/
|
||||
|
||||
import { getAccessToken } from '../../../shared/components/rstack-identity';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
interface DraftData {
|
||||
id: string;
|
||||
|
|
@ -74,7 +75,7 @@ export class FolkNewsletterManager extends HTMLElement {
|
|||
}
|
||||
|
||||
private apiBase(): string {
|
||||
return `/${this._space}/rsocials/api/newsletter`;
|
||||
return `${getModuleApiBase("rsocials")}/api/newsletter`;
|
||||
}
|
||||
|
||||
private async apiFetch(path: string, opts: RequestInit = {}): Promise<Response> {
|
||||
|
|
|
|||
|
|
@ -848,7 +848,7 @@ export class FolkThreadBuilder extends HTMLElement {
|
|||
postizBtn.textContent = 'Sending...';
|
||||
(postizBtn as HTMLButtonElement).disabled = true;
|
||||
try {
|
||||
const res = await fetch(`/${this._space}/rsocials/api/postiz/threads`, {
|
||||
const res = await fetch(`${this.basePath}api/postiz/threads`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tweets, type: 'draft' }),
|
||||
|
|
@ -1002,7 +1002,7 @@ export class FolkThreadBuilder extends HTMLElement {
|
|||
roPostizBtn.textContent = 'Sending...';
|
||||
(roPostizBtn as HTMLButtonElement).disabled = true;
|
||||
try {
|
||||
const res = await fetch(`/${this._space}/rsocials/api/postiz/threads`, {
|
||||
const res = await fetch(`${this.basePath}api/postiz/threads`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tweets: t.tweets, type: 'draft' }),
|
||||
|
|
|
|||
|
|
@ -1364,7 +1364,7 @@ routes.post("/api/campaign/wizard", async (c) => {
|
|||
(d.campaignWizards as any)[wizardId] = wizard;
|
||||
});
|
||||
|
||||
return c.json({ wizardId, url: `/${space}/rsocials/campaign-wizard/${wizardId}` }, 201);
|
||||
return c.json({ wizardId, url: c.get("isSubdomain") ? `/rsocials/campaign-wizard/${wizardId}` : `/${space}/rsocials/campaign-wizard/${wizardId}` }, 201);
|
||||
});
|
||||
|
||||
routes.get("/api/campaign/wizard/:id", (c) => {
|
||||
|
|
|
|||
|
|
@ -159,9 +159,10 @@ function itemToRow(item: SplatItem): SplatRow {
|
|||
/**
|
||||
* Return the subset of SplatRow fields used in list/gallery responses.
|
||||
*/
|
||||
function itemToListRow(item: SplatItem, spaceSlug?: string) {
|
||||
function itemToListRow(item: SplatItem, spaceSlug?: string, isSubdomain?: boolean) {
|
||||
const effectiveSlug = spaceSlug || item.spaceSlug;
|
||||
const fileUrl = item.filePath
|
||||
? `/${spaceSlug || item.spaceSlug}/rsplat/api/splats/${item.slug}/${item.slug}.${item.fileFormat}`
|
||||
? (isSubdomain ? `/rsplat/api/splats/${item.slug}/${item.slug}.${item.fileFormat}` : `/${effectiveSlug}/rsplat/api/splats/${item.slug}/${item.slug}.${item.fileFormat}`)
|
||||
: null;
|
||||
return {
|
||||
id: item.id,
|
||||
|
|
@ -229,7 +230,7 @@ routes.get("/api/splats", async (c) => {
|
|||
// Apply offset and limit
|
||||
const paged = items.slice(offset, offset + limit);
|
||||
|
||||
return c.json({ splats: paged.map(i => itemToListRow(i, spaceSlug)) });
|
||||
return c.json({ splats: paged.map(i => itemToListRow(i, spaceSlug, c.get("isSubdomain"))) });
|
||||
});
|
||||
|
||||
// ── API: Get splat details ──
|
||||
|
|
@ -657,7 +658,7 @@ routes.get("/api/splats/my-history", async (c) => {
|
|||
.sort((a, b) => b.createdAt - a.createdAt)
|
||||
.slice(0, 50);
|
||||
|
||||
return c.json({ splats: items.map(i => itemToListRow(i, spaceSlug)) });
|
||||
return c.json({ splats: items.map(i => itemToListRow(i, spaceSlug, c.get("isSubdomain"))) });
|
||||
});
|
||||
|
||||
// ── API: Delete splat (owner only) ──
|
||||
|
|
@ -708,7 +709,7 @@ routes.get("/", async (c) => {
|
|||
.sort((a, b) => b.createdAt - a.createdAt)
|
||||
.slice(0, 50);
|
||||
|
||||
const rows = items.map(i => itemToListRow(i, spaceSlug));
|
||||
const rows = items.map(i => itemToListRow(i, spaceSlug, c.get("isSubdomain")));
|
||||
const splatsJSON = JSON.stringify(rows);
|
||||
|
||||
const html = renderShell({
|
||||
|
|
@ -737,7 +738,7 @@ routes.get("/", async (c) => {
|
|||
});
|
||||
|
||||
// ── Shared viewer page renderer ──
|
||||
function renderViewerPage(spaceSlug: string, dataSpace: string, idOrSlug: string) {
|
||||
function renderViewerPage(spaceSlug: string, dataSpace: string, idOrSlug: string, isSubdomain?: boolean) {
|
||||
const doc = ensureDoc(dataSpace);
|
||||
const found = findItem(doc, idOrSlug);
|
||||
|
||||
|
|
@ -761,7 +762,7 @@ function renderViewerPage(spaceSlug: string, dataSpace: string, idOrSlug: string
|
|||
d.items[itemKey].viewCount += 1;
|
||||
});
|
||||
|
||||
const fileUrl = `/${spaceSlug}/rsplat/api/splats/${splat.slug}/${splat.slug}.${splat.fileFormat}`;
|
||||
const fileUrl = isSubdomain ? `/rsplat/api/splats/${splat.slug}/${splat.slug}.${splat.fileFormat}` : `/${spaceSlug}/rsplat/api/splats/${splat.slug}/${splat.slug}.${splat.fileFormat}`;
|
||||
|
||||
const html = renderShell({
|
||||
title: `${splat.title} | rSplat`,
|
||||
|
|
@ -852,7 +853,7 @@ routes.get("/:slug", async (c) => {
|
|||
|
||||
const spaceSlug = c.req.param("space") || "demo";
|
||||
const dataSpace = c.get("effectiveSpace") || spaceSlug;
|
||||
const result = renderViewerPage(spaceSlug, dataSpace, slug);
|
||||
const result = renderViewerPage(spaceSlug, dataSpace, slug, c.get("isSubdomain"));
|
||||
return c.html(result.html, result.status);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ routes.post("/api/clickup/import", async (c) => {
|
|||
const client = new ClickUpClient(accessToken);
|
||||
const host = c.req.header('host') || 'rspace.online';
|
||||
const protocol = c.req.header('x-forwarded-proto') || 'https';
|
||||
const endpoint = `${protocol}://${host}/${space}/rtasks/api/clickup/webhook`;
|
||||
const endpoint = c.get("isSubdomain") ? `${protocol}://${host}/rtasks/api/clickup/webhook` : `${protocol}://${host}/${space}/rtasks/api/clickup/webhook`;
|
||||
try {
|
||||
const wh = await client.createWebhook(
|
||||
conn.clickup.teamId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { DocumentId } from "../../../shared/local-first/document";
|
|||
import { TourEngine } from "../../../shared/tour-engine";
|
||||
import { ViewHistory } from "../../../shared/view-history.js";
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
import { rspaceNavUrl } from '../../../shared/url-helpers';
|
||||
|
||||
class FolkTripsPlanner extends HTMLElement {
|
||||
private shadow: ShadowRoot;
|
||||
|
|
@ -1361,8 +1362,7 @@ GUIDELINES:
|
|||
const accepted = this._aiGeneratedItems.filter(i => i.accepted);
|
||||
if (accepted.length === 0) return;
|
||||
sessionStorage.setItem('rtrips-canvas-export', JSON.stringify(accepted.map(i => ({ type: i.type, props: i.props }))));
|
||||
const nav = (window as any).__rspaceNavUrl;
|
||||
window.location.href = (nav ? nav(this.space, 'rspace') : `/${this.space}/rspace`) + '#trip-import';
|
||||
window.location.href = rspaceNavUrl(this.space, 'rspace') + '#trip-import';
|
||||
}
|
||||
|
||||
private goBack() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
* action buttons (accept/decline/cancel/complete), endorsement prompt.
|
||||
*/
|
||||
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
const STATUS_STYLES: Record<string, { bg: string; fg: string; label: string }> = {
|
||||
pending: { bg: 'rgba(245,158,11,0.15)', fg: '#f59e0b', label: 'Pending' },
|
||||
accepted: { bg: 'rgba(52,211,153,0.15)', fg: '#34d399', label: 'Accepted' },
|
||||
|
|
@ -51,7 +53,7 @@ class FolkRentalRequest extends HTMLElement {
|
|||
if (!rentalId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${space}/rvnb/api/rentals/${rentalId}`);
|
||||
const res = await fetch(`${getModuleApiBase("rvnb")}/api/rentals/${rentalId}`);
|
||||
if (res.ok) {
|
||||
this.#data = await res.json();
|
||||
this.#render();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
* owner name + trust badge, sleeps, mileage policy, pickup location, endorsement count.
|
||||
*/
|
||||
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
const ECONOMY_COLORS: Record<string, { bg: string; fg: string; label: string; icon: string }> = {
|
||||
gift: { bg: 'rgba(52,211,153,0.12)', fg: '#34d399', label: 'Gift', icon: '\u{1F49A}' },
|
||||
exchange: { bg: 'rgba(96,165,250,0.12)', fg: '#60a5fa', label: 'Exchange', icon: '\u{1F91D}' },
|
||||
|
|
@ -58,7 +60,7 @@ class FolkVehicleCard extends HTMLElement {
|
|||
if (!vehicleId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/${space}/rvnb/api/vehicles/${vehicleId}`);
|
||||
const res = await fetch(`${getModuleApiBase("rvnb")}/api/vehicles/${vehicleId}`);
|
||||
if (res.ok) {
|
||||
this.#data = await res.json();
|
||||
this.#render();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { TourStep } from '../../../shared/tour-engine';
|
|||
import './folk-vehicle-card';
|
||||
import './folk-rental-request';
|
||||
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
|
||||
import { getModuleApiBase } from "../../../shared/url-helpers";
|
||||
|
||||
const VNB_TOUR_STEPS: TourStep[] = [
|
||||
{ target: '.vnb-search', title: 'Search', message: 'Filter by vehicle type, dates, or economy model.' },
|
||||
|
|
@ -94,9 +95,9 @@ class FolkVnbView extends HTMLElement {
|
|||
async #loadData() {
|
||||
try {
|
||||
const [vehiclesRes, rentalsRes, statsRes] = await Promise.all([
|
||||
fetch(`/${this.#space}/rvnb/api/vehicles`),
|
||||
fetch(`/${this.#space}/rvnb/api/rentals`),
|
||||
fetch(`/${this.#space}/rvnb/api/stats`),
|
||||
fetch(`${getModuleApiBase("rvnb")}/api/vehicles`),
|
||||
fetch(`${getModuleApiBase("rvnb")}/api/rentals`),
|
||||
fetch(`${getModuleApiBase("rvnb")}/api/stats`),
|
||||
]);
|
||||
|
||||
if (vehiclesRes.ok) {
|
||||
|
|
@ -467,7 +468,7 @@ class FolkVnbView extends HTMLElement {
|
|||
|
||||
async #handleRentalAction(rentalId: string, action: string) {
|
||||
try {
|
||||
const res = await fetch(`/${this.#space}/rvnb/api/rentals/${rentalId}/${action}`, {
|
||||
const res = await fetch(`${getModuleApiBase("rvnb")}/api/rentals/${rentalId}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
|
@ -483,7 +484,7 @@ class FolkVnbView extends HTMLElement {
|
|||
|
||||
async #handleRentalMessage(rentalId: string, body: string) {
|
||||
try {
|
||||
const res = await fetch(`/${this.#space}/rvnb/api/rentals/${rentalId}/messages`, {
|
||||
const res = await fetch(`${getModuleApiBase("rvnb")}/api/rentals/${rentalId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ body, sender_name: 'You' }),
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ routes.post("/api/proposals", async (c) => {
|
|||
// Notify space members about the new proposal
|
||||
import('../rinbox/agent-notify').then(({ sendSpaceNotification }) => {
|
||||
sendSpaceNotification(space_slug, `New Proposal: ${title}`,
|
||||
`<h3>${title}</h3>${description ? `<p>${description}</p>` : ''}<p><a href="https://rspace.online/${space_slug}/rvote">Vote in rVote</a></p>`
|
||||
`<h3>${title}</h3>${description ? `<p>${description}</p>` : ''}<p><a href="https://${space_slug}.rspace.online/rvote">Vote in rVote</a></p>`
|
||||
).catch(() => {});
|
||||
}).catch(() => {});
|
||||
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@ function generateFallbackResponse(
|
|||
|
||||
for (const m of modules) {
|
||||
if (q.includes(m.id) || q.includes(m.name.toLowerCase())) {
|
||||
return `**${m.name}** ${m.icon} — ${m.description}. You can access it at /${space || "personal"}/${m.id}.`;
|
||||
return `**${m.name}** ${m.icon} — ${m.description}. You can access it at /${m.id}.`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ clickupOAuthRoutes.get('/callback', async (c) => {
|
|||
});
|
||||
|
||||
// Redirect back to rTasks
|
||||
const redirectUrl = `/${state.space}/rtasks?connected=clickup`;
|
||||
const redirectUrl = c.get("isSubdomain") ? `/rtasks?connected=clickup` : `/${state.space}/rtasks?connected=clickup`;
|
||||
return c.redirect(redirectUrl);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ googleOAuthRoutes.get('/callback', async (c) => {
|
|||
};
|
||||
});
|
||||
|
||||
const redirectUrl = `/${state.space}/rnotes?connected=google`;
|
||||
const redirectUrl = c.get("isSubdomain") ? `/rnotes?connected=google` : `/${state.space}/rnotes?connected=google`;
|
||||
return c.redirect(redirectUrl);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ notionOAuthRoutes.get('/callback', async (c) => {
|
|||
});
|
||||
|
||||
// Redirect back to rNotes
|
||||
const redirectUrl = `/${state.space}/rnotes?connected=notion`;
|
||||
const redirectUrl = c.get("isSubdomain") ? `/rnotes?connected=notion` : `/${state.space}/rnotes?connected=notion`;
|
||||
return c.redirect(redirectUrl);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
*/
|
||||
|
||||
import { isAuthenticated, getAccessToken, getUsername } from "./rstack-identity";
|
||||
import { rspaceNavUrl, getCurrentModule as getModule } from "../url-helpers";
|
||||
import { rspaceNavUrl, getCurrentModule as getModule, getModuleApiBase } from "../url-helpers";
|
||||
|
||||
interface SpaceInfo {
|
||||
slug: string;
|
||||
|
|
@ -1208,7 +1208,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
const savedVal = modules.find(m => m.id === modId)?.settings?.[key] || '';
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const nbRes = await fetch(`/${space}/rnotes/api/notebooks`, {
|
||||
const nbRes = await fetch(`${getModuleApiBase("rnotes")}/api/notebooks`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (nbRes.ok) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import { getSession, getAccessToken } from "./rstack-identity";
|
||||
import { getModuleApiBase } from "../url-helpers";
|
||||
|
||||
interface MemberInfo {
|
||||
did: string;
|
||||
|
|
@ -95,7 +96,7 @@ export class RStackUserDashboard extends HTMLElement {
|
|||
async #fetchMembers() {
|
||||
this.#membersLoading = true;
|
||||
try {
|
||||
const res = await fetch(`/${encodeURIComponent(this.space)}/api/space-members`);
|
||||
const res = await fetch(`${getModuleApiBase("rspace")}/api/space-members`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.#members = data.members || [];
|
||||
|
|
|
|||
|
|
@ -118,3 +118,22 @@ export function rspaceNavUrl(space: string, moduleId: string): string {
|
|||
// Localhost/dev
|
||||
return `/${space}/${moduleId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API base path for a module, subdomain-aware.
|
||||
*
|
||||
* On subdomain: returns `/{moduleId}` (e.g. `/rcart`)
|
||||
* On localhost: returns `/{space}/{moduleId}` (e.g. `/jeff/rcart`)
|
||||
*
|
||||
* Uses regex on the current pathname so it works regardless of routing mode.
|
||||
*/
|
||||
export function getModuleApiBase(moduleId: string): string {
|
||||
const path = window.location.pathname;
|
||||
const re = new RegExp(`^(\\/[^/]+)?\\/${moduleId}`);
|
||||
const match = path.match(re);
|
||||
if (match) return match[0];
|
||||
// Fallback: subdomain → /{moduleId}, else /{firstSegment}/{moduleId}
|
||||
if (isSubdomain() || isBareDomain()) return `/${moduleId}`;
|
||||
const space = path.split("/").filter(Boolean)[0] || "demo";
|
||||
return `/${space}/${moduleId}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue