mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Merge pull request #44 from ArnasDon/fix/dashboard-activity-feed-pagination
fix(dashboard): paginate recent activity feed with a size picker
This commit is contained in:
commit
eb8d0b3ac6
@ -84,7 +84,10 @@ export default function DashboardPage() {
|
|||||||
.catch((err) => console.error('[dashboard] response time failed:', err))
|
.catch((err) => console.error('[dashboard] response time failed:', err))
|
||||||
.finally(() => setResponseTimeLoading(false))
|
.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))
|
.then((a) => setActivity(a))
|
||||||
.catch((err) => console.error('[dashboard] activity failed:', err))
|
.catch((err) => console.error('[dashboard] activity failed:', err))
|
||||||
.finally(() => setActivityLoading(false))
|
.finally(() => setActivityLoading(false))
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
@ -20,6 +21,9 @@ interface ActivityFeedProps {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZES = [5, 10, 20, 50] as const
|
||||||
|
type PageSize = (typeof PAGE_SIZES)[number]
|
||||||
|
|
||||||
interface KindTheme {
|
interface KindTheme {
|
||||||
icon: ComponentType<{ className?: string }>
|
icon: ComponentType<{ className?: string }>
|
||||||
/** Tailwind classes for the round icon badge + label color. */
|
/** Tailwind classes for the round icon badge + label color. */
|
||||||
@ -35,6 +39,20 @@ const KIND_THEME: Record<ActivityKind, KindTheme> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
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<PageSize>(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 (
|
return (
|
||||||
<section className="rounded-xl border border-slate-800 bg-slate-900">
|
<section className="rounded-xl border border-slate-800 bg-slate-900">
|
||||||
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4">
|
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4">
|
||||||
@ -62,44 +80,75 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-slate-800">
|
<>
|
||||||
{items.map((it, i) => {
|
<ul className="divide-y divide-slate-800">
|
||||||
const theme = KIND_THEME[it.kind]
|
{visible.map((it, i) => {
|
||||||
const Icon = theme.icon
|
const theme = KIND_THEME[it.kind]
|
||||||
// Alternating row background for light scanability — dark-theme
|
const Icon = theme.icon
|
||||||
// translation of the spec's white / #f9fafb stripes.
|
// Alternating row background for scanability — dark-theme
|
||||||
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-slate-900/40'
|
// translation of the spec's white / #f9fafb stripes.
|
||||||
const row = (
|
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-slate-900/40'
|
||||||
<div className="flex items-center gap-3 px-5 py-2.5">
|
const row = (
|
||||||
<span
|
<div className="flex items-center gap-3 px-5 py-2.5">
|
||||||
className={cn(
|
<span
|
||||||
'flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full',
|
className={cn(
|
||||||
theme.badge,
|
'flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full',
|
||||||
|
theme.badge,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-sm text-slate-200">
|
||||||
|
{it.text}
|
||||||
|
</span>
|
||||||
|
<span className="flex-shrink-0 text-xs text-slate-500 tabular-nums">
|
||||||
|
{relativeTime(it.at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<li key={it.id} className={cn(stripe, 'transition-colors hover:bg-slate-800/40')}>
|
||||||
|
{it.href ? (
|
||||||
|
<Link href={it.href} className="block">
|
||||||
|
{row}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
row
|
||||||
)}
|
)}
|
||||||
>
|
</li>
|
||||||
<Icon className="h-3.5 w-3.5" />
|
)
|
||||||
</span>
|
})}
|
||||||
<span className="min-w-0 flex-1 truncate text-sm text-slate-200">
|
</ul>
|
||||||
{it.text}
|
<footer className="flex items-center justify-between border-t border-slate-800 px-5 py-3 text-xs">
|
||||||
</span>
|
<span className="text-slate-500 tabular-nums">
|
||||||
<span className="flex-shrink-0 text-xs text-slate-500 tabular-nums">
|
Showing {visible.length} of {totalLoaded}
|
||||||
{relativeTime(it.at)}
|
{totalLoaded === 50 ? '+' : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<div className="flex items-center gap-1">
|
||||||
)
|
<span className="mr-1 text-slate-500">Show</span>
|
||||||
return (
|
{PAGE_SIZES.map((size, i) => {
|
||||||
<li key={it.id} className={cn(stripe, 'transition-colors hover:bg-slate-800/40')}>
|
const disabled = !isSizeUseful(size, i)
|
||||||
{it.href ? (
|
return (
|
||||||
<Link href={it.href} className="block">
|
<button
|
||||||
{row}
|
key={size}
|
||||||
</Link>
|
type="button"
|
||||||
) : (
|
onClick={() => setPageSize(size)}
|
||||||
row
|
disabled={disabled}
|
||||||
)}
|
className={cn(
|
||||||
</li>
|
'rounded-md px-2 py-1 font-medium tabular-nums transition-colors',
|
||||||
)
|
pageSize === size
|
||||||
})}
|
? 'bg-slate-700 text-white'
|
||||||
</ul>
|
: 'text-slate-400 hover:bg-slate-800 hover:text-white',
|
||||||
|
disabled && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-slate-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{size}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user