Leave requests
Approve or archive a request; each toast offers Undo for 8 seconds.
- Sara MalikAnnual leave · 14 to 18 Oct · 5 days
- Luis OrtegaSick leave · 8 Oct · 1 day
- Chloe MartinRemote work · 21 to 25 Oct
- Yuki TanakaAnnual leave · 3 to 7 Nov · 5 days
toasts.tsx
"use client";
import { useState } from "react";
import { LuArchive, LuCheck } from "react-icons/lu";
import { Avatar, Badge, Button, IconButton, ToastProvider, useToast } 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",
},
];
interface Request {
id: string;
name: string;
initials: string;
detail: string;
}
const REQUESTS: Request[] = [
{ id: "r1", person: PEOPLE[1], detail: "Annual leave · 14 to 18 Oct · 5 days" },
{ id: "r2", person: PEOPLE[4], detail: "Sick leave · 8 Oct · 1 day" },
{ id: "r3", person: PEOPLE[6], detail: "Remote work · 21 to 25 Oct" },
{ id: "r4", person: PEOPLE[5], detail: "Annual leave · 3 to 7 Nov · 5 days" },
].flatMap(({ id, person, detail }) =>
person ? [{ id, name: person.name, initials: person.initials, detail }] : [],
);
function Requests() {
const toasts = useToast();
const [rows, setRows] = useState(REQUESTS);
const [approved, setApproved] = useState<string[]>([]);
const restore = (request: Request) => {
setRows((current) =>
REQUESTS.filter((item) => item.id === request.id || current.includes(item)),
);
};
const approve = (request: Request) => {
setApproved((ids) => [...ids, request.id]);
toasts.add({
tone: "success",
title: `Approved ${request.name.split(" ")[0]}'s request`,
action: {
label: "Undo",
onAction: () => {
setApproved((ids) => ids.filter((id) => id !== request.id));
},
},
});
};
const archive = (request: Request) => {
setRows((current) => current.filter((item) => item.id !== request.id));
toasts.add({
title: "Request archived",
action: {
label: "Undo",
onAction: () => {
restore(request);
},
},
});
};
return (
<ul className="m-0 list-none overflow-hidden rounded-xl border border-default bg-surface p-0">
{rows.map((request) => {
const isApproved = approved.includes(request.id);
return (
<li
key={request.id}
className="flex flex-wrap items-center gap-3 border-b border-subtle p-4 last:border-b-0"
>
<Avatar size="md" initials={request.initials} isDecorative />
<div className="grid min-w-0 flex-1 basis-40">
<span className="text-body-strong text-primary">{request.name}</span>
<span className="text-body-small text-secondary">{request.detail}</span>
</div>
{isApproved ? (
<Badge tone="success" label="Approved" />
) : (
<Button
size="sm"
variant="secondary"
leadingIcon={<LuCheck />}
onPress={() => {
approve(request);
}}
>
Approve
</Button>
)}
<IconButton
variant="ghost"
icon={<LuArchive />}
aria-label={`Archive ${request.name}'s request`}
onPress={() => {
archive(request);
}}
/>
</li>
);
})}
{rows.length === 0 ? (
<li className="p-4 text-body-default text-secondary">
No requests left. Undo to bring one back.
</li>
) : null}
</ul>
);
}
/** Toasts: short confirmations with Undo after approving or archiving a leave request. */
export default function Toasts() {
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-3xl gap-4">
<header className="grid gap-1">
<h2 className="text-heading-h2 text-primary">Leave requests</h2>
<p className="text-body-default text-secondary">
Approve or archive a request; each toast offers Undo for 8 seconds.
</p>
</header>
<ToastProvider aria-label="Notifications">
<Requests />
</ToastProvider>
</div>
</div>
);
}
More states and system messages blocks
- Empty statesFirst use, no results and all done.
- Loading skeletonsPage and panel placeholders while data loads.
- Error pagesNot found, no access and something went wrong.
- Success confirmationA modal that confirms a finished task.
- Confirm a destructive actionName what will be deleted before deleting it.
- Maintenance and offlineTell people about downtime or lost connection.
- Long-running jobProgress for exports, imports and uploads.