Skills Table¶
Navigation: Home → Features & Components → Skills Table
Table of Contents¶
- Introduction
- Features
- Architecture
- TanStack React Table Integration
- Column Definitions
- State Management
- Sorting Implementation
- Filtering & Search
- Pagination
- Actions Dropdown
- Responsive Design
- Usage Examples
- See Also
- Next Steps
Introduction¶
The Skills Data Table (components/skills-data-table.tsx) is an interactive, feature-rich table component that displays programming skills alongside the projects where they're used. Built with @tanstack/react-table, it provides professional data management capabilities including sorting, filtering, and pagination.
Key Capabilities¶
- ✅ Sortable Columns: Click headers to sort by skill name or project count
- ✅ Search/Filter: Real-time skill name filtering
- ✅ Pagination: 10 skills per page with navigation controls
- ✅ Column Visibility: Toggle column visibility via dropdown
- ✅ Project Dropdown: Quick access to project pages
- ✅ Featured Project Grouping: Separates featured from other projects
- ✅ Responsive: Adapts to mobile and desktop layouts
Features¶
Visual Overview¶
graph TB
SkillsTable[Skills Data Table] --> Search[Search Input]
SkillsTable --> ColumnToggle[Column Visibility]
SkillsTable --> TableView[Table Display]
SkillsTable --> Pagination[Pagination Controls]
TableView --> SkillCol[Skill Name Column]
TableView --> ProjectCol[Projects Column]
TableView --> ActionsCol[Actions Column]
SkillCol --> Sorting[Sortable Header]
ProjectCol --> BadgeCount[Project Count Badge]
ProjectCol --> SortLogic[Custom Sort Logic]
ActionsCol --> Dropdown[Projects Dropdown]
Dropdown --> FeaturedGroup[Featured Projects]
Dropdown --> OtherGroup[Other Projects]
Dropdown --> ProjectLinks[Project Links]
Search --> Filter[Column Filter State]
Filter --> Display[Filtered Rows]
Pagination --> PageInfo[Page Indicator]
Pagination --> NavButtons[Prev/Next Buttons]
style SkillsTable fill:#e1f5ff
style TableView fill:#d4edda
style Dropdown fill:#fff3cd
Architecture¶
Component Structure¶
components/skills-data-table.tsx
├── Column Definitions (exported)
│ ├── name: Skill name with sort
│ ├── projects: Project count with badge
│ └── actions: Dropdown menu
├── SkillsDataTable Component
│ ├── React Table Hook
│ ├── State Management
│ ├── Search Input
│ ├── Column Visibility Dropdown
│ ├── Table Rendering
│ └── Pagination Controls
Dependencies¶
{
"@tanstack/react-table": "^8.x", // Core table logic
"lucide-react": "^0.x" // Icons (ArrowUpDown, ChevronDown, etc.)
}
TanStack React Table Integration¶
Imports¶
```typescript:1:40:components/skills-data-table.tsx "use client";
import * as React from "react"; import { ColumnDef, ColumnFiltersState, SortingState, VisibilityState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowUpDown, ChevronDown, ExternalLink, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Skill } from "@/lib/skills"; import Link from "next/link"; import { Project } from "@/lib/projects";
### Table Initialization
```typescript:161:189:components/skills-data-table.tsx
export function SkillsDataTable({ data }: DataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([{ id: "projects", desc: true }]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
initialState: {
pagination: {
pageSize: 10, // Show 10 skills per page
},
},
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
// ... render
}
Default Sorting¶
The table initializes with descending sort by project count:
This ensures skills used in the most projects appear first.
Column Definitions¶
Name Column¶
```typescript:42:58:components/skills-data-table.tsx
export const columns: ColumnDef**Features:**
- Clickable header for alphabetical sorting
- `ArrowUpDown` icon indicates sortability
- Bold text (`font-medium`) for emphasis
### Projects Column
```typescript:59:87:components/skills-data-table.tsx
{
accessorKey: "projects",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="text-card-foreground !px-0"
>
Projects
<ArrowUpDown className="size-4" />
</Button>
);
},
cell: ({ row }) => {
const projects = row.getValue("projects") as Project[];
return (
<Badge variant="outline">
{projects.length} project{projects.length !== 1 ? "s" : ""}
</Badge>
);
},
sortingFn: (rowA, rowB) => {
const projectsA = rowA.getValue("projects") as Project[];
const projectsB = rowA.getValue("projects") as Project[];
return projectsA.length - projectsB.length;
},
},
Features:
- Custom
sortingFncompares project array lengths - Badge component displays count with proper pluralization
outlinevariant for subtle appearance
Actions Column¶
```typescript:88:154:components/skills-data-table.tsx { id: "actions", enableHiding: false, cell: ({ row }) => { const projects = row.getValue("projects") as Project[]; const featuredProjects = projects.filter( (project) => project.order !== undefined && project.order >= 1 && project.order <= 6 ); const nonFeaturedProjects = projects.filter( (project) => featuredProjects.indexOf(project) === -1 );
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="size-8">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>View Projects</DropdownMenuLabel>
{/* Featured Projects */}
{featuredProjects.length > 0 && (
<>
{nonFeaturedProjects.length > 0 && (
<DropdownMenuLabel className="text-muted-foreground text-xs font-medium">
Featured Projects
</DropdownMenuLabel>
)}
{featuredProjects.map((project, i) => (
<DropdownMenuItem key={project.slug + i} asChild>
<Link href={`/projects/${project.slug}`} target="_blank">
<ExternalLink className="size-4" />
{project.title}
</Link>
</DropdownMenuItem>
))}
</>
)}
{/* Separator */}
{featuredProjects.length > 0 && nonFeaturedProjects.length > 0 && (
<DropdownMenuSeparator />
)}
{/* Non-Featured Projects */}
{nonFeaturedProjects.length > 0 && (
<>
{featuredProjects.length > 0 && (
<DropdownMenuLabel className="text-muted-foreground text-xs font-medium">
Other Projects
</DropdownMenuLabel>
)}
{nonFeaturedProjects.map((project, i) => (
<DropdownMenuItem key={project.slug + i} asChild>
<Link href={`/projects/${project.slug}`} target="_blank">
<ExternalLink className="size-4" />
{project.title}
</Link>
</DropdownMenuItem>
))}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
**Featured Project Logic:**mermaid
graph LR
Projects[All Projects] --> Filter{Has order 1-6?}
Filter -->|Yes| Featured[Featured Projects]
Filter -->|No| Other[Other Projects]
Featured --> Group1[Group 1 in Dropdown]
Other --> Group2[Group 2 in Dropdown]
Group1 --> Separator[Separator]
Separator --> Group2
```
State Management¶
State Variables¶
const [sorting, setSorting] = React.useState<SortingState>([...]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
| State | Type | Purpose |
|---|---|---|
sorting |
SortingState |
Tracks which column is sorted and direction |
columnFilters |
ColumnFiltersState |
Stores active filter values (search) |
columnVisibility |
VisibilityState |
Controls which columns are visible |
rowSelection |
object |
Tracks selected rows (currently unused) |
State Flow¶
graph TD
UserAction[User Action] --> StateUpdate{Update Type?}
StateUpdate -->|Sort| UpdateSorting[setSorting]
StateUpdate -->|Filter| UpdateFilters[setColumnFilters]
StateUpdate -->|Visibility| UpdateVisibility[setColumnVisibility]
UpdateSorting --> TableRender[Table Re-render]
UpdateFilters --> TableRender
UpdateVisibility --> TableRender
TableRender --> NewData[Display Updated Data]
Sorting Implementation¶
Toggle Sorting¶
Clicking a column header toggles between three states:
- Ascending: A → Z or 0 → 9
- Descending: Z → A or 9 → 0
- None: Original order
Visual Indicator¶
The ArrowUpDown icon is always visible, indicating the column is sortable. The table doesn't show sort direction visually (could be enhanced).
Custom Sort Function¶
The projects column uses a custom sorting function to compare array lengths:
sortingFn: (rowA, rowB) => {
const projectsA = rowA.getValue("projects") as Project[];
const projectsB = rowB.getValue("projects") as Project[];
return projectsA.length - projectsB.length;
};
Filtering & Search¶
Search Input¶
```typescript:192:201:components/skills-data-table.tsx
**How It Works:**
1. Input binds to `name` column filter value
2. Typing updates `columnFilters` state
3. TanStack Table's `getFilteredRowModel` automatically filters rows
4. Case-insensitive substring matching (default behavior)
### Filter Status Display
```typescript:269:277:components/skills-data-table.tsx
<div className="text-muted-foreground flex flex-1 gap-2 text-sm">
<span>
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</span>
-
<span className="italic">
{table.getFilteredRowModel().rows.length === data.length
? `${data.length} skill${data.length !== 1 ? "s" : ""}`
: `${table.getFilteredRowModel().rows.length} of ${data.length} skills`}
Shows either:
- "50 skills" (no filter active)
- "12 of 50 skills" (filter active)
Pagination¶
Pagination Controls¶
``typescript:268:297:components/skills-data-table.tsx
<div className="flex items-center justify-between space-x-2 py-4">
<div className="text-muted-foreground flex flex-1 gap-2 text-sm">
<span>
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</span>
-
<span className="italic">
{table.getFilteredRowModel().rows.length === data.length
?${data.length} skill${data.length !== 1 ? "s" : ""}:${table.getFilteredRowModel().rows.length} of ${data.length} skills`}
### Page Size
Set in `initialState`:
```typescript
initialState: {
pagination: {
pageSize: 10, // 10 skills per page
},
}
Navigation Logic¶
table.previousPage(): Go to previous pagetable.nextPage(): Go to next pagetable.getCanPreviousPage(): Disable button on first pagetable.getCanNextPage(): Disable button on last page
Actions Dropdown¶
Project Grouping Algorithm¶
flowchart TD
Start[Get Projects for Skill] --> Split{Split Projects}
Split --> Featured[Featured: order 1-6]
Split --> Other[Other: No order or >6]
Featured --> CheckBoth{Both groups exist?}
Other --> CheckBoth
CheckBoth -->|Yes| ShowLabel1[Show 'Featured Projects' label]
CheckBoth -->|No| SkipLabel1[No label]
ShowLabel1 --> RenderFeatured[Render Featured Links]
SkipLabel1 --> RenderFeatured
RenderFeatured --> Separator{Show separator?}
Separator -->|Both groups| ShowSep[Show separator]
Separator -->|One group| SkipSep[No separator]
ShowSep --> ShowLabel2[Show 'Other Projects' label]
SkipSep --> CheckOther{Other projects exist?}
ShowLabel2 --> RenderOther[Render Other Links]
CheckOther -->|Yes| RenderOther
CheckOther -->|No| End[End]
RenderOther --> End
Dropdown UI Structure¶
┌─────────────────────────┐
│ View Projects │ ← Label
├─────────────────────────┤
│ Featured Projects │ ← Group Label (conditional)
│ 🔗 Project A │ ← Featured project link
│ 🔗 Project B │
├─────────────────────────┤ ← Separator (conditional)
│ Other Projects │ ← Group Label (conditional)
│ 🔗 Project C │ ← Other project link
│ 🔗 Project D │
└─────────────────────────┘
Responsive Design¶
Mobile Considerations¶
The table uses shadcn/ui's responsive Table component which:
- Adds horizontal scroll on narrow screens
- Maintains column alignment
- Preserves interactive elements (buttons, dropdowns)
Responsive Container¶
```typescript:227:267:components/skills-data-table.tsx
## Usage Examples
### Basic Usage
```typescript
import { getSkills } from "@/lib/skills";
import { SkillsDataTable } from "@/components/skills-data-table";
export default async function SkillsPage() {
const skills = await getSkills();
return (
<div className="container mx-auto py-10">
<h1 className="mb-6 text-3xl font-bold">My Skills</h1>
<SkillsDataTable data={skills} />
</div>
);
}
With Section Wrapper¶
<section id="skills" className="py-20">
<div className="container mx-auto">
<h2 className="mb-8 text-center text-4xl font-bold">Skills & Technologies</h2>
<p className="text-muted-foreground mb-12 text-center">
Technologies I've worked with across different projects
</p>
<SkillsDataTable data={skills} />
</div>
</section>
See Also¶
- Skills Data Generation - How skills are extracted from projects
- TanStack Table Docs - Official documentation
- Badge Overflow - Technology badge display
- Custom Components - Other custom components
Next Steps¶
- Enhance Sorting: Add visual indicators for sort direction (up/down arrows)
- Add Filters: Implement project count range filters (e.g., "1-3 projects", "4+ projects")
- Persist State: Save filter/sort preferences to localStorage
- Export Function: Add CSV/JSON export capability
- Keyboard Navigation: Enhance accessibility with keyboard shortcuts
Last Updated: 2024-02-09
Related Docs: Components | Data Flow | Testing