Team map
Where your team works, with clusters.
Team map
Selecting a person highlights their marker and centres the map; filters apply to both.
team-map.tsx
"use client";
import { useState } from "react";
import { LuMail, LuMapPin, LuUsers } from "react-icons/lu";
import {
Avatar,
Button,
Drawer,
FilterChip,
ListItem,
MapMarker,
MapMarkerPerson,
MapMarkerPlace,
MapMarkers,
MapView,
SearchField,
type MapMarkerWorkStatus,
} from "@nexera-ui/react";
/** Northwind's London HQ, 18 Canal Street, Islington. */
const HQ = [-0.1057, 51.5323] as const;
/** You (a fixed sample position; use the browser's location, with consent, in your app). */
const YOU = [-0.1049, 51.5316] as const;
/** Positions are [lng, lat]. Locations are illustrative, never live tracking. */
const PEOPLE = [
{
name: "Ayesha Khan",
initials: "AK",
role: "Senior product designer",
detail: "Floor 3, design pod",
status: "working",
lngLat: [-0.1055, 51.5325],
},
{
name: "Bilal Ahmed",
initials: "BA",
role: "Backend engineer",
detail: "Floor 2, platform",
status: "working",
lngLat: [-0.106, 51.532],
},
{
name: "Omar Farooq",
initials: "OF",
role: "Data analyst",
detail: "Remote, Bermondsey",
status: "remote",
lngLat: [-0.0634, 51.4979],
},
{
name: "Hina Raza",
initials: "HR",
role: "HR business partner",
detail: "On leave until Monday",
status: "on-leave",
lngLat: [-0.1032, 51.5462],
},
{
name: "Sara Malik",
initials: "SM",
role: "Payroll specialist",
detail: "Client visit, Southwark",
status: "working",
lngLat: [-0.0991, 51.5035],
},
] as const satisfies readonly { status: MapMarkerWorkStatus; [key: string]: unknown }[];
const FILTERS = [
["On site", "working"],
["Remote", "remote"],
["On leave", "on-leave"],
] as const;
const STATUSES = ["working", "remote", "on-leave", "offline"] as const;
/** Team map: where your team works today, with a synced people list, clusters and a profile drawer. */
export default function TeamMap() {
const [selected, setSelected] = useState("Ayesha Khan");
const [query, setQuery] = useState("");
const [filter, setFilter] = useState<MapMarkerWorkStatus | null>(null);
const [profileOpen, setProfileOpen] = useState(false);
const visible = PEOPLE.filter(
(p) => p.name.toLowerCase().includes(query.toLowerCase()) && (!filter || p.status === filter),
);
const person = PEOPLE.find((p) => p.name === selected) ?? PEOPLE[0];
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<section aria-labelledby="team-map-title" className="mx-auto grid w-full max-w-6xl gap-4">
<header className="grid gap-1">
<h2 id="team-map-title" className="text-heading-h2 text-primary">
Team map
</h2>
<p className="text-body-default text-secondary">
Selecting a person highlights their marker and centres the map; filters apply to both.
</p>
</header>
<div className="grid overflow-hidden rounded-xl border border-default bg-surface @3xl:grid-cols-[20rem_minmax(0,1fr)]">
<aside
aria-label="People"
className="flex min-w-0 flex-col gap-3 border-b border-default p-4 @3xl:border-e @3xl:border-b-0"
>
<SearchField
aria-label="Search people"
placeholder="Search people"
value={query}
onChange={setQuery}
/>
<div className="flex flex-wrap gap-1.5">
{FILTERS.map(([label, status]) => (
<FilterChip
key={status}
label={label}
isSelected={filter === status}
onChange={(on) => {
setFilter(on ? status : null);
}}
/>
))}
</div>
<ul className="m-0 grid list-none gap-1 p-0">
{visible.map((p) => (
<li key={p.name}>
<Button
variant="ghost"
aria-pressed={selected === p.name}
onPress={() => {
setSelected(p.name);
}}
className={`h-auto w-full justify-start p-2 ${selected === p.name ? "bg-nav-active-bg" : ""}`}
>
<span className="flex items-center gap-2.5">
<Avatar size="md" initials={p.initials} isDecorative />
<span className="flex flex-col items-start text-start">
<span
className={selected === p.name ? "text-nav-active-fg" : "text-primary"}
>
{p.name}
</span>
<span
className={`text-body-caption ${selected === p.name ? "text-nav-active-fg" : "text-muted"}`}
>
{p.detail}
</span>
</span>
</span>
</Button>
</li>
))}
</ul>
{visible.length === 0 ? (
<p role="status" className="text-body-small text-muted">
No people match your filters.
</p>
) : null}
<Drawer
title={person.name}
subtitle={person.role}
isOpen={profileOpen}
onOpenChange={setProfileOpen}
trigger={
<Button variant="secondary" className="mt-auto">
View {person.name.split(" ")[0]}’s profile
</Button>
}
primaryAction={
<Button size="md" leadingIcon={<LuMail />} slot="close">
Send a message
</Button>
}
>
<div className="grid gap-4">
<div className="flex justify-center py-2">
<MapMarkerPerson
name={person.name}
initials={person.initials}
workStatus={person.status}
isSelected
/>
</div>
<ul className="m-0 grid list-none p-0">
<li>
<ListItem
icon={<LuMapPin />}
title={person.detail}
subtitle="Today’s location"
/>
</li>
<li>
<ListItem
icon={<LuUsers />}
title="Northwind London"
subtitle="Home office · 18 Canal Street"
/>
</li>
<li>
<ListItem
icon={<LuMail />}
title={`${person.name.toLowerCase().replace(" ", ".")}@northwind.io`}
subtitle="Work email"
/>
</li>
</ul>
</div>
</Drawer>
</aside>
<div className="h-80 @3xl:h-[30rem]">
<MapView
aria-label="Where the team works today"
defaultCenter={[-0.095, 51.522]}
defaultZoom={12.4}
selectedId={selected}
locate
userLocation={YOU}
height="100%"
className="rounded-none border-0"
>
<MapMarker id="hq" lngLat={HQ}>
<MapMarkerPlace name="Northwind London HQ" headcount={24} />
</MapMarker>
<MapMarkers
items={visible}
getId={(p) => p.name}
getLngLat={(p) => p.lngLat}
getCategory={(p) => p.status}
categories={STATUSES}
clusterLabel={(n) => `${String(n)} people, zoom in`}
renderMarker={(p, { isSelected }) => (
<MapMarkerPerson
name={p.name}
initials={p.initials}
workStatus={p.status}
isSelected={isSelected}
onPress={() => {
setSelected(p.name);
}}
/>
)}
/>
</MapView>
</div>
</div>
</section>
</div>
);
}