Pull to refresh feed
Feed with 'new posts' and loading states.
Company feed
- People Ops · 2 h ago
- Engineering · 4 h ago
- Finance · yesterday
- Design · yesterday
pull-to-refresh-feed.tsx
"use client";
import { useState } from "react";
import { LuArrowUp, LuRefreshCw } from "react-icons/lu";
import {
Avatar,
Button,
FeedStatus,
IconButton,
ListItem,
Skeleton,
TopAppBar,
} from "@nexera-ui/react";
interface Post {
id: string;
initials: string;
title: string;
subtitle: string;
}
const FIRST: Post[] = [
{
id: "a1",
initials: "YT",
title: "Yuki Tanaka welcomed 3 new joiners",
subtitle: "People Ops · 2 h ago",
},
{
id: "a2",
initials: "LO",
title: "Luis Ortega shipped the new leave calendar",
subtitle: "Engineering · 4 h ago",
},
{
id: "a3",
initials: "CM",
title: "Chloe Martin shared the Q3 headcount report",
subtitle: "Finance · yesterday",
},
{
id: "a4",
initials: "AK",
title: "Ayesha Khan is running a design critique",
subtitle: "Design · yesterday",
},
];
const NEWER: Post[] = [
{
id: "n1",
initials: "MJ",
title: "Mia Jensen posted the offsite agenda",
subtitle: "Product · just now",
},
{
id: "n2",
initials: "OH",
title: "Omar Haddad joined Customer as Support lead",
subtitle: "Customer · 5 min ago",
},
{
id: "n3",
initials: "SM",
title: "Sara Malik: payroll closes Friday at 17:00",
subtitle: "People Ops · 12 min ago",
},
];
const OLDER: Post[] = [
{
id: "o1",
initials: "AR",
title: "Ahmed Raza opened two engineering roles",
subtitle: "Engineering · 2 days ago",
},
{
id: "o2",
initials: "YT",
title: "Benefits enrolment opens next week",
subtitle: "People Ops · 3 days ago",
},
];
// ponytail: fixed delays stand in for the network; replace `later` with your fetch.
const later = (fn: () => void) => window.setTimeout(fn, 900);
/** Pull to refresh feed: a "new posts" pill, skeletons while refreshing and a status line at the end of the feed. */
export default function PullToRefreshFeed() {
const [posts, setPosts] = useState(FIRST);
const [hasNew, setHasNew] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [more, setMore] = useState<"idle" | "loading" | "end">("idle");
const refresh = () => {
if (refreshing) return;
setRefreshing(true);
later(() => {
setPosts((current) => (hasNew ? [...NEWER, ...current] : current));
setHasNew(false);
setRefreshing(false);
});
};
const loadMore = () => {
setMore("loading");
later(() => {
setPosts((current) => [...current, ...OLDER]);
setMore("end");
});
};
return (
<div className="@container bg-page px-4 py-8">
<div className="mx-auto flex w-full max-w-sm flex-col overflow-hidden rounded-xl border border-default bg-page shadow-md">
<TopAppBar
title="Company feed"
headingLevel={2}
platform="ios"
actions={
<IconButton
variant="ghost"
platform="ios"
icon={<LuRefreshCw />}
aria-label="Refresh feed"
isLoading={refreshing}
loadingLabel="Refreshing"
onPress={refresh}
/>
}
/>
<div className="grid gap-3 px-4 pt-2 pb-6" aria-busy={refreshing}>
{hasNew && !refreshing ? (
<Button
size="sm"
leadingIcon={<LuArrowUp />}
onPress={refresh}
className="justify-self-center rounded-full"
>
3 new posts
</Button>
) : null}
{refreshing ? (
<div className="grid gap-3">
<Skeleton type="avatar-row" />
<Skeleton type="avatar-row" />
<Skeleton type="avatar-row" />
</div>
) : null}
<ul className="m-0 list-none overflow-hidden rounded-lg border border-default bg-surface p-0">
{posts.map((post) => (
<li key={post.id} className="border-b border-subtle last:border-b-0">
<ListItem
href={`#post-${post.id}`}
title={post.title}
subtitle={post.subtitle}
leading={<Avatar size="lg" initials={post.initials} isDecorative />}
/>
</li>
))}
</ul>
{more === "idle" ? (
<Button variant="secondary" platform="ios" onPress={loadMore} className="w-full">
Load older posts
</Button>
) : (
<FeedStatus status={more === "loading" ? "loading" : "end"}>
{more === "end" ? "You're all caught up · 9 posts this week" : undefined}
</FeedStatus>
)}
</div>
</div>
</div>
);
}