import { useState, useMemo, useRef, useEffect } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable, type SortingState, type ColumnFiltersState } from '@tanstack/react-table'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ExternalLink, Twitter, Check, X } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import type { TwitterProject } from '@/lib/csv-loader'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Select } from './ui/select'; import { markProjectAsSeen, markProjectAsUnseen, isProjectSeen } from '@/lib/seen-projects'; interface ProjectsTableProps { projects: TwitterProject[]; title: string; showUrlColumn?: boolean; onSeenStatusChange?: () => void; } const columnHelper = createColumnHelper(); export function ProjectsTable({ projects, title, showUrlColumn = true, onSeenStatusChange }: ProjectsTableProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); const [seenProjects, setSeenProjects] = useState>(new Set()); const parentRef = useRef(null); useEffect(() => { // Update seen projects state when component mounts or projects change const updateSeenState = () => { const seen = new Set(); projects.forEach(project => { if (isProjectSeen(project.id)) { seen.add(project.id); } }); setSeenProjects(seen); }; updateSeenState(); }, [projects]); const toggleSeen = (projectId: string) => { const isCurrentlySeen = seenProjects.has(projectId); if (isCurrentlySeen) { markProjectAsUnseen(projectId); setSeenProjects(prev => { const newSet = new Set(prev); newSet.delete(projectId); return newSet; }); } else { markProjectAsSeen(projectId); setSeenProjects(prev => new Set(prev).add(projectId)); } onSeenStatusChange?.(); }; const columns = useMemo(() => [ columnHelper.display({ id: 'seen', header: "Seen", cell: ({ row }) => { const projectId = row.original.id; const isSeen = seenProjects.has(projectId); return (
toggleSeen(projectId)} />
); }, size: 80, }), columnHelper.accessor('author_name', { header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.author_name.charAt(0).toUpperCase()}
{row.original.author_name}
@{row.original.author_screen_name}
), size: 180, }), columnHelper.accessor('project_description', { header: ({ column }) => ( ), cell: ({ row }) => (

{row.original.project_description}

), size: 300, }), columnHelper.display({ id: 'links', header: 'Links', cell: ({ row }) => (
{showUrlColumn && (
{row.original.project_url ? ( Project ) : ( No URL )}
)}
{row.original.original_tweet_url ? ( Tweet ) : ( No Tweet )}
), size: 90, }), columnHelper.accessor('media_thumbnail', { header: 'Media', cell: ({ row }) => (
{row.original.media_thumbnail ? ( Project preview ) : (
No media
)}
), size: 90, }), columnHelper.accessor('created_at', { header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.created_at && !isNaN(new Date(row.original.created_at).getTime()) ? formatDistanceToNow(new Date(row.original.created_at), { addSuffix: true }) : 'N/A'}
), size: 120, }), columnHelper.accessor('favorite_count', { header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.favorite_count.toLocaleString()}
), size: 80, }), size: 120, }), ], [showUrlColumn, seenProjects]); const table = useReactTable({ data: projects, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, state: { sorting, columnFilters, globalFilter, }, }); const { rows } = table.getRowModel(); const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 88, overscan: 5, }); const categories = useMemo(() => { const cats = Array.from(new Set(projects.map(p => p.category || 'Uncategorized'))); return cats.sort(); }, [projects]); // Calculate total width const totalWidth = table.getHeaderGroups()[0]?.headers.reduce((acc, header) => acc + header.getSize(), 0) || 880; return (

{title}

{table.getFilteredRowModel().rows.length} of {projects.length} projects
{/* Filters */}
setGlobalFilter(e.target.value)} className="pl-10" />
{/* Table Container */}
{/* Fixed Header */}
{table.getHeaderGroups().map((headerGroup) => (
`${h.getSize()}px`).join(' ') }}> {headerGroup.headers.map((header) => (
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
))}
))}
{/* Virtual Scrollable Body */}
{virtualizer.getVirtualItems().map((virtualRow) => { const row = rows[virtualRow.index]; return (
`${cell.column.getSize()}px`).join(' ') }} > {row.getVisibleCells().map((cell) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
))}
); })}
); }