import { useEffect, useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Plus, Loader2, AlertCircle } from "lucide-react"; import { Button } from "@/client/components/ui/button"; import { slugify } from "@/lib/slugify"; import { useCreateWorkspace } from "@/client/hooks/use-create-workspace"; import { useDocumentTitle } from "@/client/hooks/use-document-title"; import { api } from "@/client/lib/api"; import { resolveRootWorkspaceDecision } from "@/client/lib/root-workspace-gateway"; import { SESSION_MODES } from "@/client/lib/constants"; import { useAuthStore } from "@/client/stores/auth-store"; import { waitForWorkspaceLocalHydration } from "@/client/stores/bootstrap"; import { useWorkspaceDirectoryStore, selectMemberWorkspaces } from "@/client/stores/workspace-directory"; import { directoryCommands } from "@/client/stores/db/workspace-directory"; import { useWorkspaceNavigationStore, selectLastVisitedWorkspaceId } from "@/client/stores/workspace-navigation"; import { useSharedInboxStore, selectSharedInboxItems } from "@/client/stores/shared-inbox"; import { fetchSharedInbox } from "@/client/hooks/use-shared-inbox"; type RootViewState = "loading" | "empty" | "unavailable"; export function EmptyWorkspaceView() { useDocumentTitle(undefined); const navigate = useNavigate(); const sessionMode = useAuthStore((s) => s.sessionMode); const [view, setView] = useState("loading"); const [showForm, setShowForm] = useState(false); const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const { createWorkspace, isCreating } = useCreateWorkspace(); useEffect(() => { let cancelled = false; async function loadWorkspaceGateway() { setView("loading"); // Settle any router-driven rehydrate before reading the projection // stores. Prevents a non-local -> local in-app transition from // mistaking an unhydrated projection for an empty cache. await waitForWorkspaceLocalHydration(); if (cancelled) return; const lastVisitedWorkspaceId = selectLastVisitedWorkspaceId(useWorkspaceNavigationStore.getState()); const cachedWorkspaces = selectMemberWorkspaces(useWorkspaceDirectoryStore.getState()); const cachedSharedItems = selectSharedInboxItems(useSharedInboxStore.getState()); let liveWorkspaces: Awaited> | null = null; try { liveWorkspaces = await api.workspaces.list(); } catch { // Cached workspace and inbox state still decide the root route when live listing is unavailable. } if (cancelled) return; const decision = resolveRootWorkspaceDecision({ lastVisitedWorkspaceId, cachedWorkspaces, liveWorkspaces, }); if (liveWorkspaces !== null) { await directoryCommands.replaceAll(liveWorkspaces); } if (decision.kind === "redirect") { navigate({ to: "/$workspaceSlug", params: { workspaceSlug: decision.workspace.slug }, replace: true, }); return; } if (decision.kind === "empty") { let sharedItems = cachedSharedItems; try { const response = await fetchSharedInbox(); sharedItems = response.items; } catch { // Fall back to cached inbox } if (cancelled) return; if (sharedItems.length > 0) { navigate({ to: "/shared-with-me", replace: true }); return; } setView("empty"); return; } // Offline with no cached member workspaces: check cached shared inbox if (cachedSharedItems.length > 0) { navigate({ to: "/shared-with-me", replace: true }); return; } setView("unavailable"); } loadWorkspaceGateway(); return () => { cancelled = true; }; }, [navigate]); if (view === "loading") { return (

Loading workspaces...

); } if (view === "unavailable") { const isExpired = sessionMode === SESSION_MODES.EXPIRED; return (

Couldn't load your workspaces

{isExpired ? "Sign in again to refresh your workspaces." : "Check your connection and try again."}

{isExpired && ( )}
); } return (

No workspaces yet

Create one, or wait for an invite.

{showForm ? (
{ setName(e.target.value); setSlug(slugify(e.target.value)); }} className="rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-200 outline-none focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/30" /> setSlug(e.target.value)} className="rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-300 outline-none focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/30" />
) : ( )}
); }