nyuchimzizi
Mzizi — an open-architecture project of the Bundu Foundation, operated and developed by Nyuchi. Built on the Five African Minerals palette.
Built by Nyuchi Africav4.0.39
Standard feed page orchestrator composing header, search/filter bar, NyuchiListView infinite scroll feed, pull-to-refresh, empty state, and FAB create button into a complete screen. Every feed screen across every mini-app uses this layout — Pulse home, News feed, Circles timeline, BushTrade listings, Jobs board, Nhimbe events, Places directory. The developer provides the data source, card renderer, and filter configuration. The orchestrator handles layout, loading, errors, and responsive behaviour.
View the full component source code below.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { useNyuchiHarness } from "@/lib/harness"
/* ═══════════════════════════════════════════════════════════════
NYUCHI FEED PAGE — Layer 6 Page Orchestrator
The standard feed screen. Every mini-app's primary list view
uses this orchestrator. Composes:
L7 Shell: header, bottom-nav (via parent layout)
L5 Resilience: NyuchiSection error boundaries per section
L3 Brand: nyuchi-listing-card (or custom renderer),
nyuchi-empty-state, nyuchi-search-view
L2 Primitives: filter-bar, infinite-scroll, pull-to-refresh
L1 Tokens: --brand-accent, responsive breakpoints
✅ HARNESS ✅ TOKENS ✅ RESPONSIVE ✅ ERROR BOUNDARIES
═══════════════════════════════════════════════════════════════ */
interface FilterConfig {
key: string
label: string
options: { value: string; label: string }[]
}
interface NyuchiFeedPageProps<T = any> {
/** Page title shown in header area */
title: string
/** Subtitle or description */
subtitle?: string
/** Data items to render */
items: T[]
/** Render function for each item */
renderItem: (item: T, index: number) => React.ReactNode
/** Loading state */
loading?: boolean
/** Load more handler for infinite scroll */
onLoadMore?: () => void
/** Whether there are more items to load */
hasMore?: boolean
/** Pull-to-refresh handler */
onRefresh?: () => void
/** Search handler */
onSearch?: (query: string) => void
/** Filter configuration */
filters?: FilterConfig[]
/** Active filters */
activeFilters?: Record<string, string>
/** Filter change handler */
onFilterChange?: (key: string, value: string) => void
/** FAB create action */
onCreateAction?: () => void
/** FAB label */
createLabel?: string
/** Empty state props */
emptyIcon?: React.ReactNode
emptyTitle?: string
emptyDescription?: string
/** Skeleton count when loading */
skeletonCount?: number
className?: string
children?: React.ReactNode
}
export function NyuchiFeedPage<T>({
title,
subtitle,
items,
renderItem,
loading = false,
onLoadMore,
hasMore = false,
onRefresh,
onSearch,
filters,
activeFilters,
onFilterChange,
onCreateAction,
createLabel = "Create",
emptyIcon,
emptyTitle = "Nothing here yet",
emptyDescription,
skeletonCount = 5,
className,
children,
}: NyuchiFeedPageProps<T>) {
const { log } = useNyuchiHarness("feed-page")
return (
<div data-slot="nyuchi-feed-page" data-portal="https://design.nyuchi.com/components/nyuchi-feed-page" className={cn("min-h-screen bg-background", className)}>
<div className="mx-auto max-w-2xl px-4 pb-24 sm:px-6">
{/* Page header */}
<div className="sticky top-14 z-40 bg-background/80 backdrop-blur-xl py-4">
<h1 className="text-xl font-bold text-foreground">{title}</h1>
{subtitle && <p className="text-sm text-muted-foreground mt-0.5">{subtitle}</p>}
</div>
{/* Search + filters */}
{(onSearch || filters) && (
<div className="space-y-3 mb-4">
{onSearch && (
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground text-sm">🔍</span>
<input
type="text"
placeholder={`Search ${title.toLowerCase()}...`}
onChange={e => onSearch(e.target.value)}
className="h-12 w-full rounded-full bg-muted pl-11 pr-4 text-sm text-foreground placeholder:text-muted-foreground outline-none border border-border focus:border-[var(--brand-accent,var(--status-success, var(--color-malachite, #64FFDA)))] transition-colors"
/>
</div>
)}
{filters && filters.length > 0 && (
<div className="flex gap-1.5 overflow-x-auto pb-1 scrollbar-none">
{filters.map(f => (
<select key={f.key} value={activeFilters?.[f.key] || ""} onChange={e => onFilterChange?.(f.key, e.target.value)}
className="shrink-0 h-9 rounded-full bg-muted px-3 text-xs text-muted-foreground border border-border outline-none">
<option value="">{f.label}</option>
{f.options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
))}
</div>
)}
</div>
)}
{/* Injected children (announcements, banners) */}
{children}
{/* Feed content */}
{loading ? (
<div className="space-y-3">
{Array.from({ length: skeletonCount }).map((_, i) => (
<div key={i} className="h-24 rounded-[var(--radius-lg,14px)] bg-muted animate-pulse" />
))}
</div>
) : items.length > 0 ? (
<div className="space-y-3">
{items.map((item, i) => renderItem(item, i))}
{hasMore && onLoadMore && (
<button onClick={onLoadMore} className="flex h-12 w-full items-center justify-center rounded-full bg-muted text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
Load more
</button>
)}
</div>
) : (
<div className="py-16 flex flex-col items-center text-center">
{emptyIcon && <div className="mb-4 flex size-16 items-center justify-center rounded-full bg-[var(--brand-accent,var(--status-success, var(--color-malachite, #64FFDA)))]/10 text-2xl">{emptyIcon}</div>}
<h3 className="text-base font-semibold text-foreground">{emptyTitle}</h3>
{emptyDescription && <p className="text-sm text-muted-foreground mt-2 max-w-[280px]">{emptyDescription}</p>}
{onCreateAction && (
<button onClick={onCreateAction} className="mt-6 h-12 rounded-full bg-[var(--brand-accent,var(--status-success, var(--color-malachite, #64FFDA)))] text-[#0A0A0A] px-6 text-[13px] font-medium hover:opacity-80 transition-opacity">
{createLabel}
</button>
)}
</div>
)}
</div>
{/* FAB */}
{onCreateAction && items.length > 0 && (
<button onClick={onCreateAction} aria-label={createLabel}
className="fixed bottom-24 right-5 z-40 flex size-14 items-center justify-center rounded-full bg-[var(--brand-accent,var(--status-success, var(--color-malachite, #64FFDA)))] text-[#0A0A0A] shadow-lg hover:opacity-80 transition-opacity md:hidden">
<span className="text-xl font-bold">+</span>
</button>
)}
</div>
)
}
npx shadcn@latest add https://mzizi.dev/api/v1/ui/nyuchi-feed-pageFetch this component's metadata and source code from the registry API.
/api/v1/ui/nyuchi-feed-page