Workspace switcher
Switch organisation or create a new one.
Northwind Europe
Business plan · data stored in EU (Frankfurt)
- People
- 248
- Open requests
- 14
- Next pay run
- 31 October
workspace-switcher.tsx
"use client";
import { useState, type SyntheticEvent } from "react";
import { LuPlus, LuSettings } from "react-icons/lu";
import {
Button,
Input,
MenuItem,
Modal,
Select,
SelectOption,
WorkspaceSwitcher,
} from "@nexera-ui/react";
interface Org {
id: string;
name: string;
plan: string;
initials: string;
people: number;
region: string;
}
const INITIAL: [Org, ...Org[]] = [
{
id: "eu",
name: "Northwind Europe",
plan: "Business plan",
initials: "NE",
people: 248,
region: "EU (Frankfurt)",
},
{
id: "uk",
name: "Northwind UK",
plan: "Business plan",
initials: "NU",
people: 112,
region: "UK (London)",
},
{
id: "apac",
name: "Northwind APAC",
plan: "Starter plan",
initials: "NA",
people: 37,
region: "APAC (Tokyo)",
},
];
const REGIONS = ["EU (Frankfurt)", "UK (London)", "US (Virginia)", "APAC (Tokyo)"];
/** Workspace switcher: move between Northwind organisations, or create a new one. */
export default function WorkspaceSwitcherBlock() {
const [orgs, setOrgs] = useState(INITIAL);
const [current, setCurrent] = useState("eu");
const [isCreating, setCreating] = useState(false);
const org = orgs.find((item) => item.id === current) ?? INITIAL[0];
const onCreate = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const rawName = data.get("name");
const name = typeof rawName === "string" ? rawName.trim() : "";
if (!name) return;
const id = `org-${orgs.length + 1}`;
const region = data.get("region");
const initials = name
.split(/\s+/)
.map((word) => word[0])
.join("")
.slice(0, 2)
.toUpperCase();
setOrgs([
...orgs,
{
id,
name,
plan: "Free trial",
initials,
people: 1,
region: typeof region === "string" && region !== "" ? region : "EU (Frankfurt)",
},
]);
setCurrent(id);
setCreating(false);
};
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-4xl gap-6">
<header className="flex items-center justify-between gap-3 rounded-xl border border-default bg-surface p-2 shadow-xs">
<WorkspaceSwitcher
variant="light"
className="max-w-xs"
workspaces={orgs}
selectedKey={current}
onSelectionChange={(key) => {
setCurrent(String(key));
}}
onAction={(key) => {
if (key === "create") setCreating(true);
}}
>
<MenuItem id="create" label="Create workspace" icon={<LuPlus />} />
<MenuItem
id="settings"
label="Workspace settings"
icon={<LuSettings />}
href="#workspace-settings"
/>
</WorkspaceSwitcher>
</header>
<section aria-labelledby="workspace-title" className="grid gap-4">
<div className="grid gap-1">
<h2 id="workspace-title" className="text-heading-h2 text-primary">
{org.name}
</h2>
<p className="text-body-default text-secondary">
{org.plan} · data stored in {org.region}
</p>
</div>
<dl className="grid gap-4 @3xl:grid-cols-3">
{[
["People", org.people.toLocaleString("en-GB")],
["Open requests", org.people > 1 ? "14" : "0"],
["Next pay run", org.people > 1 ? "31 October" : "Not set up"],
].map(([term, value]) => (
<div
key={term}
className="grid gap-1 rounded-lg border border-default bg-surface p-4"
>
<dt className="text-body-small text-secondary">{term}</dt>
<dd className="text-heading-h3 text-primary">{value}</dd>
</div>
))}
</dl>
</section>
</div>
<Modal
title="Create workspace"
subtitle="A separate organisation with its own people, payroll and settings."
isOpen={isCreating}
onOpenChange={setCreating}
secondaryAction={
<Button variant="secondary" size="md" slot="close">
Cancel
</Button>
}
primaryAction={
<Button size="md" type="submit" form="create-workspace">
Create workspace
</Button>
}
>
<form id="create-workspace" className="grid gap-4" onSubmit={onCreate}>
<Input label="Workspace name" name="name" placeholder="Northwind Americas" isRequired />
<Select label="Data region" name="region" defaultValue="EU (Frankfurt)">
{REGIONS.map((region) => (
<SelectOption key={region} id={region} label={region} />
))}
</Select>
</form>
</Modal>
</div>
);
}
More app shells and navigation blocks
- Sidebar app shellSidebar with sections, nested items, badges and a user card.
- Top bar with tabsPage title, breadcrumbs, actions and section tabs.
- Mobile app shellTop app bar and a bottom navigation bar.
- Settings layoutVertical tabs on the left, a form on the right.
- Command palette⌘K search for pages, people and actions.
- Page headerTitle, description, status badge and primary actions.