"use client"; import { useEffect, useRef, useState } from "react"; import type { Entry } from "@/lib/shared/types"; type EntriesListProps = { activeGroupId: number | null; loading: boolean; entries: Entry[]; visibleEntries: Entry[]; activeFilterCount: number; onOpenDetails: (entry: Entry, index: number) => void; onClearFilters: () => void; }; const TAG_GAP_PX = 8; const TAG_CLASS = "shrink-0 rounded-full border border-accent-weak bg-accent-soft px-2 py-0.5 text-xs"; const TAG_MORE_CLASS = "shrink-0 rounded-full border border-accent-weak bg-panel px-2 py-0.5 text-xs text-soft"; function NecessityIcon({ necessity }: { necessity: Entry["necessity"] }) { if (necessity === "NECESSARY") { return ( ); } if (necessity === "UNNECESSARY") { return ( ); } return ( ); } function EntryTagsRow({ tags }: { tags: string[] }) { const containerRef = useRef(null); const moreRef = useRef(null); const tagRefs = useRef<(HTMLSpanElement | null)[]>([]); const [visibleCount, setVisibleCount] = useState(tags.length); useEffect(() => { tagRefs.current = tagRefs.current.slice(0, tags.length); setVisibleCount(tags.length); }, [tags]); useEffect(() => { if (!tags.length) return; function recomputeVisibleCount() { const containerWidth = containerRef.current?.clientWidth ?? 0; const widths = tags.map((_, index) => tagRefs.current[index]?.offsetWidth ?? 0); const moreProbe = moreRef.current; if (!containerWidth || !moreProbe || widths.some(width => !width)) { setVisibleCount(tags.length); return; } const totalTagsWidth = widths.reduce((sum, width) => sum + width, 0) + TAG_GAP_PX * Math.max(widths.length - 1, 0); if (totalTagsWidth <= containerWidth) { setVisibleCount(tags.length); return; } let nextVisibleCount = 0; let usedWidth = 0; for (let index = 0; index < widths.length; index += 1) { usedWidth += index === 0 ? widths[index] : TAG_GAP_PX + widths[index]; const remaining = widths.length - (index + 1); if (remaining <= 0) { nextVisibleCount = widths.length; break; } moreProbe.textContent = `${remaining} more...`; const moreWidth = moreProbe.offsetWidth; const totalWithMore = usedWidth + TAG_GAP_PX + moreWidth; if (totalWithMore <= containerWidth) nextVisibleCount = index + 1; else break; } setVisibleCount(nextVisibleCount); } recomputeVisibleCount(); if (typeof ResizeObserver !== "undefined") { const observer = new ResizeObserver(recomputeVisibleCount); if (containerRef.current) observer.observe(containerRef.current); return () => observer.disconnect(); } window.addEventListener("resize", recomputeVisibleCount); return () => window.removeEventListener("resize", recomputeVisibleCount); }, [tags]); if (!tags.length) { return No tags; } const visibleTags = tags.slice(0, visibleCount); const hasOverflow = visibleCount < tags.length; const remainingCount = tags.length - visibleCount; return (
{visibleTags.map((tag, index) => ( #{tag} ))} {hasOverflow ? {remainingCount} more... : null}
); } export default function EntriesList({ activeGroupId, loading, entries, visibleEntries, activeFilterCount, onOpenDetails, onClearFilters }: EntriesListProps) { return (
{!activeGroupId ? (
Select a group to view entries.
) : loading ? (
{[0, 1, 2].map(row => (
))}
) : entries.length ? ( visibleEntries.length ? ( visibleEntries.map((entry, index) => { const tags = entry.tags ?? []; return (
onOpenDetails(entry, index)} >
${entry.amountDollars.toFixed(2)}
{new Date(entry.occurredAt).toISOString().slice(0, 10)}
); }) ) : (
No matching entries.
{activeFilterCount ? ( ) : null}
) ) : (
No entries yet.
)}
); }