MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-34

1.2.6 Switcher in top-nav + settings page + invite-acceptance UI

Done
Description

Estimate: 30m · Depends on: 1.2.1, 1.2.3, 1.2.5

The user-facing React for everything 1.2 ships: workspace switcher in the top-nav, settings page at /settings/workspace (rename + leave + delete-with-double-confirmation), and the invite-acceptance landing page at /invite/accept. All three compose from the design-system primitives (Button, Input, Card, Dialog) and follow the mockups from 1.2.1.

Why one Subtask for all three UI surfaces: they share a common API client (the invite + workspace endpoints from 1.2.5) and a common visual grammar (the workspace context). Splitting them would mean three Subtasks each calling out a slightly different slice of useWorkspace() / the switcher Popover / the destructive-dialog pattern. They cluster naturally; the Subtask is sized to one focused PR review.

Top-nav minimal-then-expand pattern: this Subtask introduces a tiny app/(authed)/_components/TopNav.tsx with two slots — the workspace switcher on the left, the user menu on the right. Story 1.5 (app shell) later expands this nav with project nav, search, etc. — but composes atop the existing structure rather than replacing it. The TopNav lives in a layout file app/(authed)/layout.tsx so it appears on every authed route (the smoke /dashboard from Story 1.1.2, plus everything 1.3+ adds). The dashboard's current ad-hoc "Sign out" form becomes a user-menu item.

Switcher implementation: client component reading the user's memberships via a server-action wrapped useSWR (or equivalent cache-friendly fetch); active workspace persists via a workspace_id cookie set on switch. Switching workspaces triggers router.refresh() to revalidate server components against the new context, and clears any workspace-scoped useSWR caches.

Settings page: server component reading the active workspace via getWorkspaceContext() from 1.2.3. Renders three Card sections: Name (Input + Save server action), Members (list from findWorkspaceMemberships(workspaceId) + per-row Remove + an Invite button opening a Dialog with email Input + Send), Danger zone (Leave workspace server action with last-member guard; Delete workspace opens a Dialog matching 1.2.1's double-confirmation mockup — the user must type the workspace name to enable the destructive Button). Last-member-cannot-leave is enforced server-side AND surfaced in the UI as a disabled-with-tooltip Leave button.

Invite-acceptance page: server component at app/(authed)/invite/accept/page.tsx (NOT under (auth) — requires auth, so it's gated by the proxy.ts from Story 1.1.11). Reads ?token= from search params, calls GET /api/invites/[token] to render the workspace name + inviter; renders the "Accept invite to {Workspace}" Button. Click fires POST /api/invites/[token]/accept and redirects to /dashboard. Error states (expired, already-used, wrong-email) render full-screen with the appropriate copy from 1.2.1's mockups.

What you'll do: Create app/(authed)/layout.tsx (with the TopNav), update app/(authed)/dashboard/page.tsx to drop its inline sign-out form (now in the user menu), create app/(authed)/_components/{TopNav,WorkspaceSwitcher,UserMenu}.tsx, app/(authed)/settings/workspace/page.tsx + actions.ts (Server Actions for rename/leave/delete + send-invite), app/(authed)/invite/accept/page.tsx. Playwright E2E spec tests/e2e/workspace-flows.spec.ts covers: switcher list + switch persists across reload; rename workspace; invite + accept end-to-end (uses the file outbox from 1.1.6); leave workspace blocked when last member; delete workspace cascades.

Acceptance criteria

  • app/(authed)/layout.tsx renders TopNav on every authed route. TopNav contains the workspace switcher (left) and the user menu (right; includes Sign out, replacing the dashboard's inline form).
  • Workspace switcher: closed state shows the active workspace name + chevron; open state lists all memberships with a check on active + "Invite teammates" pinned at the bottom + a "Create workspace" entry above the invite. Selecting a workspace sets the workspace_id cookie, calls router.refresh(), and re-renders server components with the new context.
  • Settings page at /settings/workspace renders three Cards (Name, Members, Danger zone) matching 1.2.1's mockup. All three operate via Server Actions (not client-side fetches) for the form posts.
  • Rename: any member can rename; Save persists via updateWorkspace server action; success surfaces a transient toast (use the existing Radix toast primitive from 1.0.5.2).
  • Members card: lists each member's name, email, role, and a Remove button (disabled for the current user — use "Leave" instead). Includes an Invite Button opening a Dialog with email Input; Send calls the 1.2.5 invite endpoint and surfaces success/error inline.
  • Leave workspace: server action removes the current user's membership. If the user is the LAST member, the server action errors with a clear "you can't leave the last member — delete the workspace instead" message; the UI also disables the Leave Button with a tooltip explaining the same.
  • Delete workspace: opens the double-confirmation Dialog from 1.2.1. The destructive Button is disabled until the typed-name input matches the workspace name exactly. Confirming calls deleteWorkspace (server action), which deletes via Prisma with cascade. On success, redirects to whichever workspace the user has left (or to a "create your first workspace" empty state if they have none).
  • Invite-acceptance page at /invite/accept renders the workspace name + inviter, plus a single Accept Button. Click calls POST /api/invites/[token]/accept, switches the active workspace cookie to the newly-joined workspace, redirects to /dashboard.
  • Invite-acceptance error states render full-screen with copy matching 1.2.1's mockup: "This invite has expired", "This invite has already been used", "This invite is for a different email — please sign in with {email} or ask the inviter for a new link".
  • Playwright E2E spec tests/e2e/workspace-flows.spec.ts covers: sign-up → switcher shows the auto-created workspace → rename → invite a second user (read link from file outbox, sign up as second user, accept) → second user appears in members list → switcher now shows both workspaces → switch between them → second user leaves → first user deletes workspace → cascade verified by DB query.
  • All 4 quality gates green; test suite green; Playwright suite passes locally and in CI.

Context refs

  • /design/workspaces/*.png from 1.2.1 — all four mockups
  • components/ui/{Button,Input,Card,Dialog,Toast}.tsx + app/globals.css tokens
  • app/(authed)/dashboard/page.tsx — current smoke route to refactor
  • lib/workspaces/{repo,context,middleware,invites}.ts from 1.2.2 / 1.2.3 / 1.2.5
  • app/api/workspaces/* + app/api/invites/* endpoints from 1.2.4 / 1.2.5
  • tests/e2e/_helpers/{db-reset,email-capture}.ts from 1.1.7 — the E2E helper pattern this Subtask reuses
  • Next.js docs (fetched at prompt-gen time): Server Actions form patterns, router.refresh() + Server Components cache invalidation
Status
Done
Type
Sub-task