mirror of
https://github.com/FranP-code/format_twitter_projects_accounts_tweets.git
synced 2025-10-13 00:32:19 +00:00
Fix table alignment and spacing issues
This commit is contained in:
committed by
GitHub
parent
02ce586c69
commit
2670c9277c
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useRef } from 'react';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import {
|
import {
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type SortingState,
|
type SortingState,
|
||||||
type ColumnFiltersState
|
type ColumnFiltersState
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Search, Filter, ExternalLink } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ExternalLink } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import type { TwitterProject } from '@/lib/csv-loader';
|
import type { TwitterProject } from '@/lib/csv-loader';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
@@ -29,6 +29,7 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
columnHelper.accessor('author_name', {
|
columnHelper.accessor('author_name', {
|
||||||
@@ -36,7 +37,7 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
className="h-auto p-0 font-semibold hover:bg-transparent justify-start w-full"
|
||||||
>
|
>
|
||||||
Author
|
Author
|
||||||
{column.getIsSorted() === 'asc' ? (
|
{column.getIsSorted() === 'asc' ? (
|
||||||
@@ -49,23 +50,24 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3 min-w-[200px]">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
||||||
{row.original.author_name.charAt(0).toUpperCase()}
|
{row.original.author_name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<div className="font-medium">{row.original.author_name}</div>
|
<div className="font-medium truncate">{row.original.author_name}</div>
|
||||||
<div className="text-sm text-muted-foreground">@{row.original.author_screen_name}</div>
|
<div className="text-sm text-muted-foreground truncate">@{row.original.author_screen_name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
size: 200,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('project_description', {
|
columnHelper.accessor('project_description', {
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
className="h-auto p-0 font-semibold hover:bg-transparent justify-start w-full"
|
||||||
>
|
>
|
||||||
Description
|
Description
|
||||||
{column.getIsSorted() === 'asc' ? (
|
{column.getIsSorted() === 'asc' ? (
|
||||||
@@ -78,52 +80,61 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="max-w-md">
|
<div className="min-w-[300px] max-w-[400px]">
|
||||||
<p className="text-sm leading-relaxed line-clamp-3">{row.original.project_description}</p>
|
<p className="text-sm leading-relaxed line-clamp-3">{row.original.project_description}</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
size: 350,
|
||||||
}),
|
}),
|
||||||
...(showUrlColumn ? [
|
...(showUrlColumn ? [
|
||||||
columnHelper.accessor('project_url', {
|
columnHelper.accessor('project_url', {
|
||||||
header: 'Project',
|
header: 'Project',
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) => (
|
||||||
row.original.project_url ? (
|
<div className="min-w-[100px]">
|
||||||
<a
|
{row.original.project_url ? (
|
||||||
href={row.original.project_url}
|
<a
|
||||||
target="_blank"
|
href={row.original.project_url}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="inline-flex items-center space-x-1 text-primary hover:text-primary/80 transition-colors"
|
rel="noopener noreferrer"
|
||||||
>
|
className="inline-flex items-center space-x-1 text-primary hover:text-primary/80 transition-colors"
|
||||||
<ExternalLink className="w-4 h-4" />
|
>
|
||||||
<span className="text-sm">View</span>
|
<ExternalLink className="w-4 h-4" />
|
||||||
</a>
|
<span className="text-sm">View</span>
|
||||||
) : (
|
</a>
|
||||||
<span className="text-muted-foreground text-sm">No URL</span>
|
) : (
|
||||||
),
|
<span className="text-muted-foreground text-sm">No URL</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
size: 100,
|
||||||
})
|
})
|
||||||
] : []),
|
] : []),
|
||||||
columnHelper.accessor('media_thumbnail', {
|
columnHelper.accessor('media_thumbnail', {
|
||||||
header: 'Media',
|
header: 'Media',
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) => (
|
||||||
row.original.media_thumbnail ? (
|
<div className="min-w-[80px] flex justify-center">
|
||||||
<img
|
{row.original.media_thumbnail ? (
|
||||||
src={row.original.media_thumbnail}
|
<img
|
||||||
alt="Project preview"
|
src={row.original.media_thumbnail}
|
||||||
className="w-16 h-16 object-cover rounded-lg"
|
alt="Project preview"
|
||||||
loading="lazy"
|
className="w-16 h-16 object-cover rounded-lg"
|
||||||
/>
|
loading="lazy"
|
||||||
) : (
|
/>
|
||||||
<div className="w-16 h-16 bg-muted rounded-lg flex items-center justify-center">
|
) : (
|
||||||
<span className="text-xs text-muted-foreground">No media</span>
|
<div className="w-16 h-16 bg-muted rounded-lg flex items-center justify-center">
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">No media</span>
|
||||||
),
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
size: 80,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('created_at', {
|
columnHelper.accessor('created_at', {
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
className="h-auto p-0 font-semibold hover:bg-transparent justify-start w-full"
|
||||||
>
|
>
|
||||||
Date
|
Date
|
||||||
{column.getIsSorted() === 'asc' ? (
|
{column.getIsSorted() === 'asc' ? (
|
||||||
@@ -136,17 +147,18 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-sm">
|
<div className="text-sm min-w-[120px]">
|
||||||
{formatDistanceToNow(new Date(row.original.created_at), { addSuffix: true })}
|
{formatDistanceToNow(new Date(row.original.created_at), { addSuffix: true })}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
size: 120,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('favorite_count', {
|
columnHelper.accessor('favorite_count', {
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
className="h-auto p-0 font-semibold hover:bg-transparent justify-start w-full"
|
||||||
>
|
>
|
||||||
Likes
|
Likes
|
||||||
{column.getIsSorted() === 'asc' ? (
|
{column.getIsSorted() === 'asc' ? (
|
||||||
@@ -159,17 +171,18 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium min-w-[80px] text-right">
|
||||||
{row.original.favorite_count.toLocaleString()}
|
{row.original.favorite_count.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
size: 80,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('category', {
|
columnHelper.accessor('category', {
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
className="h-auto p-0 font-semibold hover:bg-transparent justify-start w-full"
|
||||||
>
|
>
|
||||||
Category
|
Category
|
||||||
{column.getIsSorted() === 'asc' ? (
|
{column.getIsSorted() === 'asc' ? (
|
||||||
@@ -182,10 +195,13 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-secondary text-secondary-foreground rounded-full">
|
<div className="min-w-[120px]">
|
||||||
{row.original.category}
|
<span className="inline-block px-2 py-1 text-xs font-medium bg-secondary text-secondary-foreground rounded-full">
|
||||||
</span>
|
{row.original.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
|
size: 120,
|
||||||
}),
|
}),
|
||||||
], [showUrlColumn]);
|
], [showUrlColumn]);
|
||||||
|
|
||||||
@@ -205,10 +221,11 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const parentRef = useState<HTMLDivElement | null>(null);
|
const { rows } = table.getRowModel();
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: table.getRowModel().rows.length,
|
count: rows.length,
|
||||||
getScrollElement: () => parentRef[0],
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 80,
|
estimateSize: () => 80,
|
||||||
overscan: 5,
|
overscan: 5,
|
||||||
});
|
});
|
||||||
@@ -256,46 +273,58 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="border rounded-lg">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<div className="overflow-auto" style={{ height: '600px' }} ref={parentRef[1]}>
|
<div className="w-full">
|
||||||
<table className="w-full">
|
{/* Header */}
|
||||||
<thead className="bg-muted/50 sticky top-0 z-10">
|
<div className="bg-muted/50 border-b">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<div key={headerGroup.id} className="flex">
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th key={header.id} className="h-12 px-4 text-left align-middle font-medium">
|
<div
|
||||||
{header.isPlaceholder
|
key={header.id}
|
||||||
? null
|
className="px-4 py-3 text-left font-medium border-r border-border last:border-r-0"
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
style={{ width: `${header.getSize()}px` }}
|
||||||
</th>
|
>
|
||||||
))}
|
{header.isPlaceholder
|
||||||
</tr>
|
? null
|
||||||
))}
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
</thead>
|
</div>
|
||||||
<tbody className="relative">
|
))}
|
||||||
<tr style={{ height: `${virtualizer.getTotalSize()}px` }}>
|
</div>
|
||||||
<td></td>
|
))}
|
||||||
</tr>
|
</div>
|
||||||
|
|
||||||
|
{/* Virtual Scrollable Body */}
|
||||||
|
<div
|
||||||
|
ref={parentRef}
|
||||||
|
className="h-[600px] overflow-auto"
|
||||||
|
>
|
||||||
|
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
|
||||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
const row = table.getRowModel().rows[virtualRow.index];
|
const row = rows[virtualRow.index];
|
||||||
return (
|
return (
|
||||||
<tr
|
<div
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="border-b transition-colors hover:bg-muted/50 absolute w-full"
|
className="absolute w-full flex border-b border-border hover:bg-muted/50 transition-colors"
|
||||||
style={{
|
style={{
|
||||||
|
height: `${virtualRow.size}px`,
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td key={cell.id} className="p-4 align-middle">
|
<div
|
||||||
|
key={cell.id}
|
||||||
|
className="px-4 py-3 border-r border-border last:border-r-0 flex items-center"
|
||||||
|
style={{ width: `${cell.column.getSize()}px` }}
|
||||||
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</div>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user