From 96debd193bdc3025c37278d9d151fe81e3b1193c Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Fri, 17 Apr 2026 22:35:42 +0300 Subject: [PATCH] fix(dashboard): paginate recent activity feed with a size picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With real data the feed showed 20 rows by default and filled most of the viewport. Switched to a 5-row default with an inline size picker (5 / 10 / 20 / 50) so users choose how much history is visible. - Dashboard page bumps the fetch from 20 → 50 so every size option is already in memory. Switching sizes is a pure client-side slice, no round trip. - ActivityFeed owns the pageSize state, renders "Showing N of M" plus a button group in the footer. The smallest option is always enabled; larger options unlock only once we have enough rows to meaningfully reveal more (e.g. "10" enables once loaded > 5). - Existing "View all" header link left alone. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(dashboard)/dashboard/page.tsx | 5 +- src/components/dashboard/activity-feed.tsx | 123 ++++++++++++++------- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index bf68834..aa68db8 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -84,7 +84,10 @@ export default function DashboardPage() { .catch((err) => console.error('[dashboard] response time failed:', err)) .finally(() => setResponseTimeLoading(false)) - void loadActivity(db, 20) + // Fetch up to 50 so the biggest page-size option in the feed + // (50 rows) is already in memory — switching sizes then becomes + // a pure client-side slice with no extra round trip. + void loadActivity(db, 50) .then((a) => setActivity(a)) .catch((err) => console.error('[dashboard] activity failed:', err)) .finally(() => setActivityLoading(false)) diff --git a/src/components/dashboard/activity-feed.tsx b/src/components/dashboard/activity-feed.tsx index 36d3da3..e26c4e6 100644 --- a/src/components/dashboard/activity-feed.tsx +++ b/src/components/dashboard/activity-feed.tsx @@ -1,6 +1,7 @@ "use client" import Link from 'next/link' +import { useState } from 'react' import { MessageSquare, UserPlus, @@ -20,6 +21,9 @@ interface ActivityFeedProps { loading: boolean } +const PAGE_SIZES = [5, 10, 20, 50] as const +type PageSize = (typeof PAGE_SIZES)[number] + interface KindTheme { icon: ComponentType<{ className?: string }> /** Tailwind classes for the round icon badge + label color. */ @@ -35,6 +39,20 @@ const KIND_THEME: Record = { } export function ActivityFeed({ items, loading }: ActivityFeedProps) { + // Start at 5 — a quick scan of the most recent events without + // dominating vertical real estate. User expands explicitly via the + // footer control when they want deeper history. + const [pageSize, setPageSize] = useState(5) + + const totalLoaded = items?.length ?? 0 + const visible = items?.slice(0, pageSize) ?? [] + // A size option is "useful" if picking it would reveal rows the + // smaller option doesn't already show. With PAGE_SIZES=[5,10,20,50]: + // "10" is useful only once we've loaded ≥6 items, "20" once ≥11, etc. + // The smallest option is always enabled. + const isSizeUseful = (size: PageSize, i: number) => + i === 0 || totalLoaded > PAGE_SIZES[i - 1] + return (
@@ -62,44 +80,75 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) { /> ) : ( -
    - {items.map((it, i) => { - const theme = KIND_THEME[it.kind] - const Icon = theme.icon - // Alternating row background for light scanability — dark-theme - // translation of the spec's white / #f9fafb stripes. - const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-slate-900/40' - const row = ( -
    - +
      + {visible.map((it, i) => { + const theme = KIND_THEME[it.kind] + const Icon = theme.icon + // Alternating row background for scanability — dark-theme + // translation of the spec's white / #f9fafb stripes. + const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-slate-900/40' + const row = ( +
      + + + + + {it.text} + + + {relativeTime(it.at)} + +
      + ) + return ( +
    • + {it.href ? ( + + {row} + + ) : ( + row )} - > - - - - {it.text} - - - {relativeTime(it.at)} - -
    - ) - return ( -
  • - {it.href ? ( - - {row} - - ) : ( - row - )} -
  • - ) - })} -
+ + ) + })} + + + )}
)