From 16e959072b7b80053426725c4f445d3951749ba2 Mon Sep 17 00:00:00 2001 From: Yahya Alhinai Date: Sun, 28 Jun 2026 04:07:10 +0000 Subject: [PATCH] feat: route pods by URL slug --- frontend/src/App.tsx | 78 ++++++++++++++++++++++++++++++++----- scripts/verify-frontend.mjs | 4 ++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3ee58fb..2cbc670 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,6 +42,26 @@ import { Skeleton } from '@/components/ui/skeleton'; const SESSION_KEY = 'podman.session'; const fmt = new Intl.NumberFormat('en', { notation: 'compact' }); +function pathPodId(): string | null { + const [segment] = window.location.pathname.split('/').filter(Boolean); + return segment ? decodeURIComponent(segment) : null; +} + +function setPodPath(podId: string, replace = false): void { + const next = `/${encodeURIComponent(podId)}`; + if (window.location.pathname === next) return; + window.history[replace ? 'replaceState' : 'pushState']({}, '', next); +} + +function setHomePath(): void { + if (window.location.pathname === '/') return; + window.history.pushState({}, '', '/'); +} + +function replacePath(path: string): void { + window.history.replaceState({}, '', path || '/'); +} + export default function App() { const [pods, setPods] = useState([]); const [loading, setLoading] = useState(true); @@ -85,13 +105,20 @@ export default function App() { return n; }); - async function connectToPod(podId: string, who: string) { - const result = await joinPod(podId, who, who); - setRoom(result.room); - setDevMode(result.mode === 'dev'); - setMember(who); - setJoinedPodId(podId); - sessionStorage.setItem(SESSION_KEY, JSON.stringify({ podId, member: who })); + async function connectToPod(podId: string, who: string, replaceRoute = false) { + const previousPath = window.location.pathname; + setPodPath(podId, replaceRoute); + try { + const result = await joinPod(podId, who, who); + setRoom(result.room); + setDevMode(result.mode === 'dev'); + setMember(who); + setJoinedPodId(podId); + sessionStorage.setItem(SESSION_KEY, JSON.stringify({ podId, member: who })); + } catch (e) { + replacePath(previousPath); + throw e; + } } useEffect(() => { @@ -124,8 +151,14 @@ export default function App() { }, [joinedPodId]); useEffect(() => { + const routedPodId = pathPodId(); const raw = sessionStorage.getItem(SESSION_KEY); - if (!raw) return; + if (!raw) { + if (routedPodId) { + setError(`Enter your name to join ${routedPodId}.`); + } + return; + } let saved: { podId: string; member: string }; try { saved = JSON.parse(raw); @@ -133,10 +166,11 @@ export default function App() { sessionStorage.removeItem(SESSION_KEY); return; } + const podId = routedPodId ?? saved.podId; setRestoring(true); void (async () => { try { - await connectToPod(saved.podId, saved.member); + await connectToPod(podId, saved.member, !!routedPodId); } catch { sessionStorage.removeItem(SESSION_KEY); } finally { @@ -145,6 +179,30 @@ export default function App() { })(); }, []); + useEffect(() => { + const onPopState = () => { + const routedPodId = pathPodId(); + if (!routedPodId) { + room?.disconnect(); + setRoom(null); + setJoinedPodId(null); + return; + } + const raw = sessionStorage.getItem(SESSION_KEY); + if (!raw) return; + try { + const saved = JSON.parse(raw) as { member: string }; + if (saved.member && routedPodId !== joinedPodId) { + void connectToPod(routedPodId, saved.member, true); + } + } catch { + sessionStorage.removeItem(SESSION_KEY); + } + }; + window.addEventListener('popstate', onPopState); + return () => window.removeEventListener('popstate', onPopState); + }, [joinedPodId, room]); + async function run(key: string, fn: () => Promise) { startPending(key); setError(null); @@ -179,6 +237,7 @@ export default function App() { run(id, async () => { await api.deletePod(id); setPods((cur) => cur.filter((x) => x.id !== id)); + if (pathPodId() === id) setHomePath(); }); const handleAddMember = (id: string, name: string) => run(id, async () => upsert(await api.addMember(id, name))); @@ -215,6 +274,7 @@ export default function App() { setRoom(null); setJoinedPodId(null); sessionStorage.removeItem(SESSION_KEY); + setHomePath(); void refresh(); } diff --git a/scripts/verify-frontend.mjs b/scripts/verify-frontend.mjs index 5ea37f1..964dda4 100644 --- a/scripts/verify-frontend.mjs +++ b/scripts/verify-frontend.mjs @@ -262,6 +262,9 @@ try { await podCard.getByPlaceholder('Your name').fill(verifyMember); await podCard.getByRole('button', { name: 'Add and join' }).click(); await page.getByRole('button', { name: 'Share screen' }).waitFor({ timeout: 15_000 }); + if (new URL(page.url()).pathname !== `/${verifyPod.id}`) { + throw new Error(`join did not update URL to /${verifyPod.id}: ${page.url()}`); + } await page.getByRole('heading', { name: 'My stream' }).waitFor({ timeout: 15_000 }); await page.getByRole('heading', { name: 'Team stream' }).waitFor({ timeout: 15_000 }); @@ -316,6 +319,7 @@ try { } await page.getByRole('button', { name: 'Leave pod' }).click(); + await page.waitForURL((url) => url.pathname === '/', { timeout: 15_000 }); if (consoleErrors.length) throw new Error(`console errors: ${consoleErrors.join(' | ')}`); if (pageErrors.length) throw new Error(`page errors: ${pageErrors.join(' | ')}`);