Invite your team
Search people and invite them in one go.
Bring your team to Northwind
It's just you so far. Invites expire after 7 days.
AK
invite-your-team.tsx
"use client";
import { useState } from "react";
import { LuUserPlus } from "react-icons/lu";
import {
Alert,
Avatar,
AvatarGroup,
Button,
Checkbox,
ListItem,
Modal,
SearchField,
} from "@nexera-ui/react";
// Sample data
interface Person {
id: string;
name: string;
initials: string;
role: string;
team: string;
email: string;
location: string;
status: "Active" | "On leave" | "Remote" | "Invited";
}
const PEOPLE: Person[] = [
{
id: "p1",
name: "Ayesha Khan",
initials: "AK",
role: "Senior product designer",
team: "Design",
email: "ayesha.khan@northwind.io",
location: "London",
status: "Active",
},
{
id: "p2",
name: "Sara Malik",
initials: "SM",
role: "Payroll specialist",
team: "People Ops",
email: "sara.malik@northwind.io",
location: "Lisbon",
status: "On leave",
},
{
id: "p3",
name: "Ahmed Raza",
initials: "AR",
role: "Engineering manager",
team: "Engineering",
email: "ahmed.raza@northwind.io",
location: "Lahore",
status: "Remote",
},
{
id: "p4",
name: "Mia Jensen",
initials: "MJ",
role: "Product manager",
team: "Product",
email: "mia.jensen@northwind.io",
location: "Copenhagen",
status: "Active",
},
{
id: "p5",
name: "Luis Ortega",
initials: "LO",
role: "Frontend engineer",
team: "Engineering",
email: "luis.ortega@northwind.io",
location: "Madrid",
status: "Active",
},
{
id: "p6",
name: "Yuki Tanaka",
initials: "YT",
role: "Recruiter",
team: "People Ops",
email: "yuki.tanaka@northwind.io",
location: "Tokyo",
status: "Active",
},
{
id: "p7",
name: "Chloe Martin",
initials: "CM",
role: "Data analyst",
team: "Finance",
email: "chloe.martin@northwind.io",
location: "Paris",
status: "Remote",
},
{
id: "p8",
name: "Omar Haddad",
initials: "OH",
role: "Support lead",
team: "Customer",
email: "omar.haddad@northwind.io",
location: "Dubai",
status: "Invited",
},
];
const [OWNER, ...DIRECTORY] = PEOPLE;
/** Invite your team: search the company directory and invite several people in one go. */
export default function InviteYourTeam() {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<Set<string>>(new Set());
const [invited, setInvited] = useState<string[]>([]);
const remaining = DIRECTORY.filter((person) => !invited.includes(person.id));
const matches = remaining.filter((person) =>
`${person.name} ${person.team} ${person.role}`
.toLowerCase()
.includes(query.trim().toLowerCase()),
);
const toggle = (id: string, isSelected: boolean) => {
setSelected((previous) => {
const next = new Set(previous);
if (isSelected) next.add(id);
else next.delete(id);
return next;
});
};
const sendInvites = () => {
setInvited((previous) => [...previous, ...selected]);
setSelected(new Set());
setQuery("");
setIsOpen(false); // replace with your invite request
};
const team = PEOPLE.filter((person) => person.id === OWNER?.id || invited.includes(person.id));
return (
<div className="@container min-h-[480px] bg-page px-4 py-8 @3xl:px-8">
<section className="mx-auto grid max-w-2xl gap-4 rounded-xl border border-default bg-surface p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="grid gap-1">
<h2 className="text-heading-h3 text-primary">Bring your team to Northwind</h2>
<p className="text-body-default text-secondary">
{team.length === 1
? "It's just you so far."
: `${team.length} people in your workspace.`}{" "}
Invites expire after 7 days.
</p>
</div>
<Button
size="md"
leadingIcon={<LuUserPlus />}
onPress={() => {
setIsOpen(true);
}}
isDisabled={remaining.length === 0}
>
Invite people
</Button>
</div>
<AvatarGroup size="md" count={6} aria-label="People in your workspace">
{team.map((person) => (
<Avatar key={person.id} initials={person.initials} label={person.name} />
))}
</AvatarGroup>
{invited.length > 0 ? (
<Alert
tone="success"
title={invited.length === 1 ? "1 invite sent" : `${invited.length} invites sent`}
description="They'll show as Invited until they sign in. This is a demo, so no email was sent."
/>
) : null}
</section>
<Modal
isOpen={isOpen}
onOpenChange={setIsOpen}
size="md"
title="Invite people"
subtitle="People from the Northwind directory who don't have access yet."
footerNote={`${selected.size} selected`}
secondaryAction={
<Button variant="secondary" size="md" slot="close">
Cancel
</Button>
}
primaryAction={
<Button size="md" isDisabled={selected.size === 0} onPress={sendInvites}>
{selected.size === 1 ? "Send 1 invite" : `Send ${selected.size} invites`}
</Button>
}
>
<div className="grid gap-4">
<SearchField
label="Search people"
placeholder="Name, team or role"
value={query}
onChange={setQuery}
/>
{matches.length === 0 ? (
<p role="status" className="py-6 text-center text-body-default text-secondary">
No one matches “{query}”.
</p>
) : (
<ul
aria-label="People you can invite"
className="m-0 grid list-none divide-y divide-subtle rounded-lg border border-default p-0"
>
{matches.map((person) => (
<li key={person.id}>
<ListItem
title={person.name}
subtitle={`${person.role} · ${person.team}`}
leading={
<span className="flex items-center gap-3">
<Checkbox
aria-label={`Invite ${person.name}`}
isSelected={selected.has(person.id)}
onChange={(isSelected) => {
toggle(person.id, isSelected);
}}
/>
<Avatar size="lg" initials={person.initials} isDecorative />
</span>
}
/>
</li>
))}
</ul>
)}
</div>
</Modal>
</div>
);
}