Team members and roles
Invite, change roles and remove members.
Members and roles
7 members in Northwind UK. Roles decide who can see payroll and approve leave.
Members
- ActiveAyesha Khan ayesha.khan@northwind.io
- On leaveSara Malik sara.malik@northwind.io
- RemoteAhmed Raza ahmed.raza@northwind.io
- ActiveMia Jensen mia.jensen@northwind.io
- ActiveLuis Ortega luis.ortega@northwind.io
- ActiveYuki Tanaka yuki.tanaka@northwind.io
- RemoteChloe Martin chloe.martin@northwind.io
Pending invites
- omar.haddad@northwind.ioInvited as Member · expires in 7 days
team-members-and-roles.tsx
"use client";
import { useState, type SyntheticEvent } from "react";
import { LuMail, LuSend } from "react-icons/lu";
import {
Avatar,
AvatarLabel,
Badge,
Button,
Input,
ListItem,
Select,
SelectOption,
} 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 ROLES = [
{ id: "owner", label: "Owner", description: "Billing and everything else" },
{ id: "admin", label: "Admin", description: "Payroll, policies and people" },
{ id: "manager", label: "Manager", description: "Approves their team's requests" },
{ id: "member", label: "Member", description: "Their own profile and requests" },
];
const INITIAL_ROLES: Record<string, string> = {
p1: "owner",
p2: "admin",
p3: "manager",
p4: "manager",
};
const STATUS_TONE = {
Active: "success",
"On leave": "leave",
Remote: "remote",
Invited: "info",
} as const;
/** Team members and roles: invite by email, change each member's role and remove members. */
export default function TeamMembersAndRoles() {
const [members, setMembers] = useState<Person[]>(
PEOPLE.filter((person) => person.status !== "Invited"),
);
const [roles, setRoles] = useState(INITIAL_ROLES);
const [invites, setInvites] = useState(
PEOPLE.filter((person) => person.status === "Invited").map((person) => person.email),
);
const [email, setEmail] = useState("");
const [notice, setNotice] = useState("");
const onInvite = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
setInvites((previous) => (previous.includes(email) ? previous : [...previous, email]));
setNotice(`Invite sent to ${email}.`); // replace with your invite request
setEmail("");
};
return (
<div className="@container min-h-[640px] bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-4xl gap-6">
<header className="grid gap-1">
<h2 className="text-heading-h2 text-primary">Members and roles</h2>
<p className="text-body-default text-secondary">
{members.length} members in Northwind UK. Roles decide who can see payroll and approve
leave.
</p>
</header>
<form
onSubmit={onInvite}
className="grid gap-3 rounded-xl border border-default bg-surface p-5 @3xl:grid-cols-[minmax(0,1fr)_12rem_auto] @3xl:items-end"
>
<Input
label="Invite by email"
type="email"
name="email"
leadingIcon={<LuMail />}
placeholder="name@northwind.io"
value={email}
onChange={setEmail}
isRequired
/>
<Select label="Role" name="role" defaultValue="member">
{ROLES.filter((role) => role.id !== "owner").map((role) => (
<SelectOption
key={role.id}
id={role.id}
label={role.label}
description={role.description}
/>
))}
</Select>
<Button type="submit" leadingIcon={<LuSend />}>
Send invite
</Button>
</form>
<p role="status" className="text-body-small text-status-success-fg empty:hidden">
{notice}
</p>
<section
aria-labelledby="members-heading"
className="grid gap-2 rounded-xl border border-default bg-surface p-5"
>
<h3 id="members-heading" className="text-heading-h4 text-primary">
Members
</h3>
<ul
aria-labelledby="members-heading"
className="m-0 grid list-none divide-y divide-subtle p-0"
>
{members.map((person) => (
<li key={person.id} className="flex flex-wrap items-center gap-x-4 gap-y-3 py-3">
<div className="min-w-0 flex-1 basis-56">
<AvatarLabel
size="lg"
name={person.name}
subtitle={person.email}
avatar={<Avatar initials={person.initials} isDecorative />}
/>
</div>
<Badge label={person.status} tone={STATUS_TONE[person.status]} />
<div className="w-40">
<Select
aria-label={`Role for ${person.name}`}
value={roles[person.id] ?? "member"}
onChange={(key) => {
if (key !== null)
setRoles((previous) => ({ ...previous, [person.id]: String(key) }));
}}
isDisabled={roles[person.id] === "owner"}
>
{ROLES.map((role) => (
<SelectOption key={role.id} id={role.id} label={role.label} />
))}
</Select>
</div>
<Button
variant="ghost"
size="sm"
aria-label={`Remove ${person.name}`}
isDisabled={roles[person.id] === "owner"}
onPress={() => {
setMembers((previous) => previous.filter((member) => member.id !== person.id));
setNotice(`${person.name} was removed. Their records stay in the directory.`);
}}
>
Remove
</Button>
</li>
))}
</ul>
</section>
<section
aria-labelledby="invites-heading"
className="grid gap-2 rounded-xl border border-default bg-surface p-5"
>
<h3 id="invites-heading" className="text-heading-h4 text-primary">
Pending invites
</h3>
{invites.length === 0 ? (
<p className="text-body-small text-secondary">No pending invites.</p>
) : null}
<ul
aria-labelledby="invites-heading"
className="m-0 grid list-none divide-y divide-subtle p-0"
>
{invites.map((invite) => (
<li key={invite}>
<ListItem
icon={<LuMail />}
title={invite}
subtitle="Invited as Member · expires in 7 days"
trailing={
<Button
variant="ghost"
size="sm"
aria-label={`Revoke invite for ${invite}`}
onPress={() => {
setInvites((previous) => previous.filter((item) => item !== invite));
}}
>
Revoke
</Button>
}
/>
</li>
))}
</ul>
</section>
</div>
</div>
);
}
More settings and preferences blocks
- Notification preferencesChoose which alerts arrive by email, push or not at all.
- AppearanceTheme, density and accent colour.
- SecurityPassword, two-step verification and active sessions.
- Billing and planCurrent plan, payment method and invoices.
- IntegrationsConnect and configure other apps.
- Delete accountA clear warning and a confirmation step.