Add a person
Create a record in a modal.
People
4 people at Northwind
- DesignAyesha KhanSenior product designer
- People OpsSara MalikPayroll specialist
- EngineeringAhmed RazaEngineering manager
- ProductMia JensenProduct manager
add-a-person.tsx
"use client";
import { useId, useState, type SyntheticEvent } from "react";
import { LuMail, LuUserPlus } from "react-icons/lu";
import {
Alert,
Avatar,
Badge,
Button,
Input,
InputPhone,
ListItem,
Modal,
Select,
SelectOption,
type InputPhoneCountry,
} 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 TEAMS = [
"Design",
"Engineering",
"Finance",
"People Ops",
"Product",
"Customer",
] as const;
const COUNTRIES: readonly InputPhoneCountry[] = [
{ code: "GB", name: "United Kingdom", dialCode: "+44" },
{ code: "PT", name: "Portugal", dialCode: "+351" },
{ code: "PK", name: "Pakistan", dialCode: "+92" },
{ code: "DK", name: "Denmark", dialCode: "+45" },
{ code: "AE", name: "United Arab Emirates", dialCode: "+971" },
];
interface Row {
id: string;
name: string;
role: string;
team: string;
}
const initials = (name: string) =>
name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("");
/** Add a person: a team list whose "Add person" button opens a modal form to create the record. */
export default function AddAPerson() {
const formId = useId();
const [open, setOpen] = useState(false);
const [added, setAdded] = useState<string | null>(null);
const [rows, setRows] = useState<Row[]>(() =>
PEOPLE.slice(0, 4).map(({ id, name, role, team }) => ({ id, name, role, team })),
);
const onSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const field = (key: string) => {
const value = data.get(key);
return typeof value === "string" ? value : "";
};
const name = field("name");
setRows((current) => [
...current,
{ id: `new-${current.length}`, name, role: field("role"), team: field("team") },
]);
setAdded(name);
setOpen(false); // replace with your create-employee request
};
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<section
aria-labelledby="people-title"
className="mx-auto grid max-w-3xl gap-5 rounded-xl border border-default bg-surface p-5 shadow-sm @3xl:p-6"
>
<header className="flex flex-wrap items-center justify-between gap-3">
<div className="grid gap-1">
<h2 id="people-title" className="text-heading-h3 text-primary">
People
</h2>
<p className="text-body-small text-secondary">{rows.length} people at Northwind</p>
</div>
<Button
size="md"
leadingIcon={<LuUserPlus />}
onPress={() => {
setOpen(true);
}}
>
Add person
</Button>
</header>
{added ? (
<Alert
tone="success"
title={`${added} was added`}
description="We sent them an invite to set up their account."
politeness="polite"
onDismiss={() => {
setAdded(null);
}}
/>
) : null}
<ul className="grid divide-y divide-subtle">
{rows.map((row) => (
<li key={row.id}>
<ListItem
title={row.name}
subtitle={row.role}
leading={<Avatar size="lg" initials={initials(row.name)} isDecorative />}
trailing={<Badge label={row.team} tone="neutral" dot={false} />}
/>
</li>
))}
</ul>
</section>
<Modal
title="Add person"
subtitle="They will get an email invite to set up their Northwind account."
size="md"
isOpen={open}
onOpenChange={setOpen}
secondaryAction={
<Button variant="secondary" size="md" slot="close">
Cancel
</Button>
}
primaryAction={
<Button type="submit" form={formId} size="md">
Add person
</Button>
}
>
<form id={formId} className="grid gap-4" onSubmit={onSubmit}>
<Input
label="Full name"
name="name"
autoComplete="name"
placeholder="e.g. Priya Shah"
isRequired
errorMessage="Enter their full name."
/>
<Input
label="Work email"
name="email"
type="email"
autoComplete="off"
leadingIcon={<LuMail />}
placeholder="priya.shah@northwind.io"
isRequired
errorMessage="Enter an email address like name@northwind.io."
/>
<div className="grid gap-4 @3xl:grid-cols-2">
<Input
label="Job title"
name="role"
autoComplete="off"
placeholder="e.g. Payroll analyst"
isRequired
errorMessage="Enter their job title."
/>
<Select
label="Team"
name="team"
placeholder="Choose a team"
isRequired
errorMessage="Choose the team they join."
>
{TEAMS.map((team) => (
<SelectOption key={team} id={team} label={team} />
))}
</Select>
</div>
<InputPhone
label="Mobile number"
name="phone"
countryFieldName="phoneCountry"
countries={COUNTRIES}
placeholder="7700 900123"
description="Optional. Used for two-step verification."
/>
</form>
</Modal>
</div>
);
}
More forms and data entry blocks
- Contact formName, email, topic and message with validation.
- Multi-step formLong forms split into steps you can go back to.
- Address and phoneCountry, address lookup and phone with dialling code.
- File uploadDrag files in and watch each one upload.
- Date range pickerChoose a start and end date on a calendar.
- Search with suggestionsType ahead and pick from matching results.
- Terms and consentScrollable terms with a required checkbox.
- Choose an optionPick one option from a short list in a dialog.
- Pick a colourBrand or label colour from swatches or a picker.