Activity feed
Newest first, with a 'new items' pill and paging.
Activity
Everything that happened at Northwind, newest first
- Today, 10:54
- Today, 09:47
activity-feed.tsx
"use client";
import { useState } from "react";
import { LuArrowUp } from "react-icons/lu";
import { Avatar, Button, CursorPager, FeedStatus, ListItem } from "@nexera-ui/react";
interface Activity {
id: string;
who: string;
initials: string;
text: string;
when: string;
}
const a = (id: string, who: string, initials: string, text: string, when: string): Activity => ({
id,
who,
initials,
text,
when,
});
const INCOMING = [
a("n3", "Omar Haddad", "OH", "accepted his invitation and joined Customer", "Today, 11:42"),
a("n2", "Sara Malik", "SM", "sent the October payroll for approval", "Today, 11:39"),
a("n1", "Yuki Tanaka", "YT", "moved Daniel Okafor to Offer for Backend engineer", "Today, 11:35"),
];
const PAGES: Activity[][] = [
[
a("e1", "Mia Jensen", "MJ", "approved Luis Ortega's leave for 19–23 October", "Today, 11:28"),
a("e2", "Chloe Martin", "CM", "published the Q3 headcount report", "Today, 10:54"),
a("e3", "Ahmed Raza", "AR", "added 2 goals to the Platform team plan", "Today, 10:12"),
a("e4", "Ayesha Khan", "AK", "updated her emergency contact", "Today, 09:47"),
],
[
a("e5", "Luis Ortega", "LO", "requested 5 days of annual leave", "Yesterday, 17:20"),
a(
"e6",
"Yuki Tanaka",
"YT",
"scheduled 4 interviews for Customer success manager",
"Yesterday, 15:03",
),
a("e7", "Sara Malik", "SM", "corrected the overtime rate for Finance", "Yesterday, 11:31"),
a("e8", "Mia Jensen", "MJ", "invited Omar Haddad to Northwind", "Monday, 16:45"),
],
[
a("e9", "Chloe Martin", "CM", "exported September expenses", "Monday, 09:15"),
a("e10", "Ahmed Raza", "AR", "completed his self-review for H2", "Friday, 14:02"),
],
];
/** Activity feed: newest first, a pill that brings in new activity without moving what you are reading, and paging to older items. */
export default function ActivityFeed() {
const [page, setPage] = useState(0);
const [fresh, setFresh] = useState<Activity[]>([]);
const waiting = INCOMING.length - fresh.length;
const items = page === 0 ? [...fresh, ...(PAGES[0] ?? [])] : (PAGES[page] ?? []);
const isLast = page === PAGES.length - 1;
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-2xl gap-4">
<header className="grid gap-1">
<h2 id="activity-title" className="text-heading-h2 text-primary">
Activity
</h2>
<p className="text-body-default text-secondary">
Everything that happened at Northwind, newest first
</p>
</header>
{page === 0 && waiting > 0 ? (
<div className="sticky top-4 z-10 flex justify-center">
<Button
size="sm"
leadingIcon={<LuArrowUp />}
className="rounded-full shadow-md"
onPress={() => {
setFresh(INCOMING);
}}
>
{waiting} new {waiting === 1 ? "update" : "updates"}
</Button>
</div>
) : null}
<ol
aria-labelledby="activity-title"
className="grid divide-y divide-subtle rounded-lg border border-default bg-surface"
>
{items.map((item) => (
<li key={item.id}>
<ListItem
href={`#activity-${item.id}`}
title={`${item.who} ${item.text}`}
subtitle={item.when}
leading={<Avatar size="lg" initials={item.initials} isDecorative />}
/>
</li>
))}
</ol>
{isLast ? (
<FeedStatus status="end">
You have reached the start of the log · 13 events since Friday
</FeedStatus>
) : null}
<CursorPager
aria-label="Activity pages"
context={page === 0 ? "Latest activity" : `Older activity, page ${page + 1}`}
{...(page === 0
? { status: "first-page" as const }
: isLast
? { status: "last-page" as const }
: {})}
onPrevious={() => {
setPage((p) => Math.max(0, p - 1));
}}
onNext={() => {
setPage((p) => Math.min(PAGES.length - 1, p + 1));
}}
previousLabel="Newer"
nextLabel="Older"
/>
</div>
</div>
);
}
More lists, tables and records blocks
- Data tableSearch, sort, select rows and page through results.
- Bulk actionsA floating bar that acts on the selected rows.
- Filter barActive filters as removable chips above results.
- Filters drawerDetailed filters in a side panel.
- People directorySearchable list of people with presence.
- Record details drawerOpen a row to see details without leaving the list.
- File browserFolders and files in a tree.
- Keyboard shortcutsA modal listing every shortcut.