From f4f1e140a9044ca98fbdf397b83479c2ec0f17a4 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 24 Feb 2026 23:17:55 -0800 Subject: [PATCH] fix: standardize SpaceSwitcher to use subdomain URLs Space links now go to . (e.g., myspace.rswag.online) instead of rspace.online/. Matches the standard rApp header pattern used across all rStack apps. Co-Authored-By: Claude Opus 4.6 --- frontend/app/layout.tsx | 2 +- frontend/components/HeaderBar.tsx | 13 +- frontend/components/SpaceSwitcher.tsx | 210 +++++++++++++++----------- 3 files changed, 124 insertions(+), 101 deletions(-) diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index ad4aa8e..1c03dc6 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -59,7 +59,7 @@ export default async function RootLayout({
- + {/* ── Main Content ────────────────────────────────── */}
{children}
diff --git a/frontend/components/HeaderBar.tsx b/frontend/components/HeaderBar.tsx index 22479c1..3923a48 100644 --- a/frontend/components/HeaderBar.tsx +++ b/frontend/components/HeaderBar.tsx @@ -8,22 +8,19 @@ import { AuthButton } from '@/components/AuthButton'; interface HeaderBarProps { name: string; logoUrl: string | null; - spaceId?: string; } -export function HeaderBar({ name, logoUrl, spaceId = 'default' }: HeaderBarProps) { +export function HeaderBar({ name, logoUrl }: HeaderBarProps) { return ( -
+
{/* Left: App switcher + Space switcher + Logo */} -
+
-
- -
+ {logoUrl ? ( diff --git a/frontend/components/SpaceSwitcher.tsx b/frontend/components/SpaceSwitcher.tsx index 9206105..d20fc68 100644 --- a/frontend/components/SpaceSwitcher.tsx +++ b/frontend/components/SpaceSwitcher.tsx @@ -2,27 +2,30 @@ import { useState, useRef, useEffect } from 'react'; -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; - interface SpaceInfo { - id: string; + slug: string; name: string; - tagline: string; - description: string; - domain: string; - logo_url: string | null; + icon?: string; + role?: string; } interface SpaceSwitcherProps { - currentSpaceId?: string; + /** Current app domain, e.g. 'rswag.online'. Space links become . */ + domain?: string; } -export function SpaceSwitcher({ currentSpaceId = 'default' }: SpaceSwitcherProps) { +export function SpaceSwitcher({ domain }: SpaceSwitcherProps) { const [open, setOpen] = useState(false); const [spaces, setSpaces] = useState([]); + const [loaded, setLoaded] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); const ref = useRef(null); - // Click-outside to close + // Derive domain from window.location if not provided + const appDomain = domain || (typeof window !== 'undefined' + ? window.location.hostname.split('.').slice(-2).join('.') + : 'rspace.online'); + useEffect(() => { function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { @@ -33,105 +36,128 @@ export function SpaceSwitcher({ currentSpaceId = 'default' }: SpaceSwitcherProps return () => document.removeEventListener('click', handleClick); }, []); - // Fetch available spaces + // Check auth status on mount useEffect(() => { - fetch(`${API_URL}/spaces`) - .then((r) => (r.ok ? r.json() : [])) - .then((data: SpaceInfo[]) => setSpaces(data)) + fetch('/api/me') + .then((r) => r.json()) + .then((data) => { + if (data.authenticated) setIsAuthenticated(true); + }) .catch(() => {}); }, []); - const currentSpace = spaces.find((s) => s.id === currentSpaceId) || { - id: 'default', - name: 'rSwag', - tagline: 'Community Merch', - domain: 'rswag.online', - logo_url: null, + const loadSpaces = async () => { + if (loaded) return; + try { + const res = await fetch('/api/spaces'); + if (res.ok) { + const data = await res.json(); + setSpaces(data.spaces || []); + } + } catch { + // API not available + } + setLoaded(true); }; - function switchSpace(spaceId: string) { - // Set cookie and reload to apply space theme - document.cookie = `space_id=${spaceId}; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`; - setOpen(false); - window.location.reload(); - } + const handleOpen = async () => { + const nowOpen = !open; + setOpen(nowOpen); + if (nowOpen && !loaded) { + await loadSpaces(); + } + }; + + /** Build URL for a space: . */ + const spaceUrl = (slug: string) => `https://${slug}.${appDomain}`; + + const mySpaces = spaces.filter((s) => s.role); + const publicSpaces = spaces.filter((s) => !s.role); return (
- {/* Trigger */} - {/* Dropdown */} {open && ( -
- {/* Header */} -
-
- Spaces -
-
- - {/* Space list */} - {spaces.length > 0 ? ( - spaces.map((s) => ( - - )) + + Create new space + + ) : ( -
- No spaces available -
- )} + <> + {mySpaces.length > 0 && ( + <> +
+ Your spaces +
+ {mySpaces.map((s) => ( + setOpen(false)} + > + {s.icon || '🌐'} + {s.name} + {s.role && ( + + {s.role} + + )} + + ))} + + )} - {/* Footer */} -
- - {spaces.length} space{spaces.length !== 1 ? 's' : ''} - - setOpen(false)} - > - + Create Space - -
+ {publicSpaces.length > 0 && ( + <> + {mySpaces.length > 0 &&
} +
+ Public spaces +
+ {publicSpaces.map((s) => ( + setOpen(false)} + > + {s.icon || '🌐'} + {s.name} + + ))} + + )} + + )}