Data Table
Data tables display information in a grid-like format of rows and columns.
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Pencil, Trash } from "@mynaui/icons-react";
import { useMemo, useState } from "react";
type Bookmark = {
id: number;
title: string;
url: string;
tags: string[];
description: string;
createdAt: string;
};
type SortColumn = keyof Bookmark;
export default function Component() {
const [bookmarks] = useState<Bookmark[]>([
{
id: 1,
title: "Vercel",
url: "https://vercel.com",
tags: ["web", "deployment"],
description:
"Vercel is a cloud platform for static sites and serverless functions.",
createdAt: "2023-05-01",
},
{
id: 2,
title: "Tailwind CSS",
url: "https://tailwindcss.com",
tags: ["css", "framework"],
description:
"Tailwind CSS is a utility-first CSS framework for rapidly building custom designs.",
createdAt: "2023-04-15",
},
{
id: 3,
title: "React",
url: "https://reactjs.org",
tags: ["javascript", "library"],
description:
"React is a JavaScript library for building user interfaces.",
createdAt: "2023-03-20",
},
{
id: 4,
title: "Next.js",
url: "https://nextjs.org",
tags: ["react", "framework"],
description:
"Next.js is a React framework that enables server-side rendering and more.",
createdAt: "2023-02-10",
},
{
id: 5,
title: "Prisma",
url: "https://www.prisma.io",
tags: ["database", "orm"],
description:
"Prisma is an open-source database toolkit that includes an ORM.",
createdAt: "2023-01-01",
},
]);
const [searchTerm, setSearchTerm] = useState("");
const [sortColumn, setSortColumn] = useState<SortColumn>("title");
const [sortDirection, setSortDirection] = useState("asc");
const filteredBookmarks = useMemo(() => {
return bookmarks.filter((bookmark) =>
bookmark.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
}, [bookmarks, searchTerm]);
const sortedBookmarks = useMemo(() => {
return filteredBookmarks.sort((a, b) => {
if (a[sortColumn] < b[sortColumn])
return sortDirection === "asc" ? -1 : 1;
if (a[sortColumn] > b[sortColumn])
return sortDirection === "asc" ? 1 : -1;
return 0;
});
}, [filteredBookmarks, sortColumn, sortDirection]);
const handleSort = (column: SortColumn) => {
if (sortColumn === column) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
setSortColumn(column);
setSortDirection("asc");
}
};
return (
<div className="mx-auto w-full max-w-6xl rounded border">
<div className="flex flex-wrap items-center justify-between gap-4 border-b p-4 md:py-2">
<h1 className="text-xl font-bold">Bookmarks</h1>
<Input
placeholder="Search bookmarks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="md:w-96"
/>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("title")}
>
Title
{sortColumn === "title" && (
<span className="ml-1">
{sortDirection === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("url")}
>
URL
{sortColumn === "url" && (
<span className="ml-1">
{sortDirection === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("tags")}
>
Tags
{sortColumn === "tags" && (
<span className="ml-1">
{sortDirection === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("description")}
>
Description
{sortColumn === "description" && (
<span className="ml-1">
{sortDirection === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("createdAt")}
>
Created
{sortColumn === "createdAt" && (
<span className="ml-1">
{sortDirection === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedBookmarks.map((bookmark) => (
<TableRow key={bookmark.id}>
<TableCell className="font-medium">{bookmark.title}</TableCell>
<TableCell>
<a
href="#"
target="_blank"
className="text-blue-500 hover:underline"
>
{bookmark.url}
</a>
</TableCell>
<TableCell className="flex flex-wrap gap-1">
{bookmark.tags.map((tag, index) => (
<Badge variant="outline" key={index}>
{tag}
</Badge>
))}
</TableCell>
<TableCell>{bookmark.description}</TableCell>
<TableCell>{bookmark.createdAt}</TableCell>
<TableCell className="flex gap-1">
<Button variant="ghost" size="icon">
<Pencil className="size-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash className="size-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
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 { ArrowUp, Pencil, Trash } from "@mynaui/icons-react";
import { useMemo, useState } from "react";
interface Tag {
name: string;
bookmarks: number;
description: string;
relatedTags: string[];
}
interface SortState {
key: keyof Tag;
order: "asc" | "desc";
}
export default function WithSort() {
const [search, setSearch] = useState<string>("");
const [sort, setSort] = useState<SortState>({ key: "name", order: "asc" });
// eslint-disable-next-line react-hooks/exhaustive-deps
const tags: Tag[] = [
{
name: "React",
bookmarks: 1234,
description: "A JavaScript library for building user interfaces",
relatedTags: ["JavaScript", "Frontend", "UI"],
},
{
name: "Node.js",
bookmarks: 2345,
description:
"A JavaScript runtime built on Chrome's V8 JavaScript engine",
relatedTags: ["JavaScript", "Backend", "Server"],
},
{
name: "Python",
bookmarks: 3456,
description:
"A high-level programming language known for its readability and versatility",
relatedTags: ["Programming", "Data Science", "Machine Learning"],
},
{
name: "Vue.js",
bookmarks: 1567,
description:
"A progressive JavaScript framework for building user interfaces",
relatedTags: ["JavaScript", "Frontend", "UI"],
},
{
name: "Ruby on Rails",
bookmarks: 2678,
description: "A server-side web application framework written in Ruby",
relatedTags: ["Ruby", "Backend", "Web Development"],
},
{
name: "Angular",
bookmarks: 3789,
description:
"A TypeScript-based web application framework for building single-page applications",
relatedTags: ["TypeScript", "Frontend", "SPA"],
},
];
const filteredTags = useMemo(() => {
return tags
.filter((tag) => {
const searchValue = search.toLowerCase();
return (
tag.name.toLowerCase().includes(searchValue) ||
tag.description.toLowerCase().includes(searchValue) ||
tag.relatedTags.some((relatedTag) =>
relatedTag.toLowerCase().includes(searchValue),
)
);
})
.sort((a, b) => {
if (sort.order === "asc") {
return a[sort.key] > b[sort.key] ? 1 : -1;
} else {
return a[sort.key] < b[sort.key] ? 1 : -1;
}
});
}, [search, sort.key, sort.order, tags]);
return (
<div className="mx-auto w-full max-w-6xl rounded border">
<div className="flex flex-wrap items-center justify-between gap-4 border-b p-4 md:py-2">
<h1 className="text-xl font-bold">Tag Cloud</h1>
<div className="flex items-center gap-2">
<Input
value={search}
placeholder="Search tags..."
onChange={(e) => setSearch(e.target.value)}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<ArrowUp className="size-4 stroke-2 text-muted-foreground" />
Sort by
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px]" align="end">
<DropdownMenuRadioGroup
value={sort.key}
onValueChange={(key) =>
setSort({ key: key as keyof Tag, order: sort.order })
}
>
<DropdownMenuRadioItem value="name">Name</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bookmarks">
Bookmarks
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="description">
Description
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={sort.order}
onValueChange={(order) =>
setSort({ key: sort.key, order: order as "asc" | "desc" })
}
>
<DropdownMenuRadioItem value="asc">
Ascending
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="desc">
Descending
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead
className="w-[200px]"
onClick={() =>
setSort({
key: "name",
order:
sort.key === "name"
? sort.order === "asc"
? "desc"
: "asc"
: "asc",
})
}
>
Tag Name
{sort.key === "name" && (
<span className="ml-1">
{sort.order === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="w-[150px] text-right"
onClick={() =>
setSort({
key: "bookmarks",
order:
sort.key === "bookmarks"
? sort.order === "asc"
? "desc"
: "asc"
: "asc",
})
}
>
Bookmarks
{sort.key === "bookmarks" && (
<span className="ml-1">
{sort.order === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="flex-1"
onClick={() =>
setSort({
key: "description",
order:
sort.key === "description"
? sort.order === "asc"
? "desc"
: "asc"
: "asc",
})
}
>
Description
{sort.key === "description" && (
<span className="ml-1">
{sort.order === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead
className="w-[200px]"
onClick={() =>
setSort({
key: "relatedTags",
order:
sort.key === "relatedTags"
? sort.order === "asc"
? "desc"
: "asc"
: "asc",
})
}
>
Related Tags
{sort.key === "relatedTags" && (
<span className="ml-1">
{sort.order === "asc" ? "\u2191" : "\u2193"}
</span>
)}
</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTags.map((tag) => (
<TableRow key={tag.name}>
<TableCell className="font-medium">{tag.name}</TableCell>
<TableCell className="text-right">
{tag.bookmarks.toLocaleString()}
</TableCell>
<TableCell>{tag.description}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
{tag.relatedTags.map((relatedTag) => (
<Badge variant="outline" key={relatedTag}>
{relatedTag}
</Badge>
))}
</div>
</TableCell>
<TableCell className="flex items-center justify-end gap-2">
<Button variant="ghost" size="icon">
<Pencil className="size-5" />
</Button>
<Button variant="ghost" size="icon">
<Trash className="size-5" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Eye, Pencil, Trash } from "@mynaui/icons-react";
import { useCallback, useMemo, useState } from "react";
const initialColumns = [
"username",
"bookmarks",
"folders",
"tags",
"joined",
"actions",
];
const usersData = [
{
username: "johndoe",
bookmarks: 25,
folders: 10,
tags: 50,
joined: "2021-03-15",
},
{
username: "janesmith",
bookmarks: 18,
folders: 7,
tags: 32,
joined: "2022-01-01",
},
{
username: "bobwilson",
bookmarks: 32,
folders: 15,
tags: 60,
joined: "2020-09-01",
},
{
username: "sarahjones",
bookmarks: 14,
folders: 5,
tags: 22,
joined: "2023-02-28",
},
{
username: "mikeanderson",
bookmarks: 28,
folders: 12,
tags: 45,
joined: "2021-11-10",
},
];
export default function WithVisibility() {
const [selectedColumns, setSelectedColumns] = useState(initialColumns);
const handleColumnToggle = useCallback((column: string) => {
setSelectedColumns((prevColumns) =>
prevColumns.includes(column)
? prevColumns.filter((col) => col !== column)
: [...prevColumns, column],
);
}, []);
const columns = useMemo(
() => ["username", "bookmarks", "folders", "tags", "joined", "actions"],
[],
);
return (
<Card className="mx-auto w-full max-w-6xl">
<CardHeader className="flex flex-row justify-between">
<div className="space-y-2">
<CardTitle>User Profile Data</CardTitle>
<CardDescription>
View and manage user profile data, including bookmarks, folders, and
tags.
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Eye className="size-4 stroke-2 text-muted-foreground" />
Show/Hide
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
{columns.map((column) => (
<DropdownMenuCheckboxItem
key={column}
checked={selectedColumns.includes(column)}
onCheckedChange={() => handleColumnToggle(column)}
>
{column.charAt(0).toUpperCase() + column.slice(1)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
{selectedColumns.map((column) => (
<TableHead key={column}>
{column.charAt(0).toUpperCase() + column.slice(1)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{usersData.map((user, index) => (
<TableRow key={index}>
{selectedColumns.map((column) => (
<TableCell key={column}>
{column === "username" ? (
<a href="#" className="font-medium">
{user.username}
</a>
) : column === "bookmarks" ? (
user.bookmarks
) : column === "folders" ? (
user.folders
) : column === "tags" ? (
user.tags
) : column === "joined" ? (
user.joined
) : column === "actions" ? (
<div className="flex items-center gap-2">
<Button variant="outline" size="icon">
<Pencil className="size-4" />
</Button>
<Button variant="outline" size="icon">
<Trash className="size-4" />
</Button>
</div>
) : null}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
"use client";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Trash } from "@mynaui/icons-react";
import { useState } from "react";
interface Task {
id: number;
task: string;
assignee: string;
due: string;
status: string;
}
export default function WithSelection() {
const tasks: Task[] = [
{
id: 1,
task: "Design landing page",
assignee: "Alice",
due: "2024-06-01",
status: "In Progress",
},
{
id: 2,
task: "Setup database",
assignee: "Bob",
due: "2024-06-05",
status: "Pending",
},
{
id: 3,
task: "Implement auth",
assignee: "Charlie",
due: "2024-06-10",
status: "Pending",
},
{
id: 4,
task: "Write documentation",
assignee: "Dana",
due: "2024-06-15",
status: "Pending",
},
];
const [selected, setSelected] = useState<number[]>([]);
const allSelected = selected.length === tasks.length;
const toggleAll = (checked: boolean) => {
setSelected(checked ? tasks.map((t) => t.id) : []);
};
const toggleOne = (id: number, checked: boolean) => {
setSelected((prev) =>
checked ? [...prev, id] : prev.filter((item) => item !== id),
);
};
const deleteSelected = () => {
// handle deletion logic
alert(`Delete tasks: ${selected.join(", ")}`);
};
return (
<div className="mx-auto w-full max-w-6xl rounded border">
<div className="flex items-center justify-between border-b p-4">
<h1 className="text-xl font-bold">Task List</h1>
<Button
variant="destructive"
disabled={!selected.length}
onClick={deleteSelected}
>
<Trash className="size-4" />
<span className="sr-only">Delete selected tasks</span>
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-4">
<Checkbox
id="select-all"
checked={allSelected}
onCheckedChange={(c) => toggleAll(Boolean(c))}
aria-label="Select all tasks"
/>
</TableHead>
<TableHead>Task</TableHead>
<TableHead>Assignee</TableHead>
<TableHead>Due</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tasks.map((task) => {
const checked = selected.includes(task.id);
return (
<TableRow key={task.id} className={checked ? "bg-muted" : undefined}>
<TableCell>
<Checkbox
id={`select-${task.id}`}
checked={checked}
onCheckedChange={(c) => toggleOne(task.id, Boolean(c))}
aria-label={`Select ${task.task}`}
/>
</TableCell>
<TableCell className="font-medium">{task.task}</TableCell>
<TableCell>{task.assignee}</TableCell>
<TableCell>{task.due}</TableCell>
<TableCell>{task.status}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}
"use client";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useState } from "react";
interface Customer {
id: number;
name: string;
email: string;
joined: string;
}
const customers: Customer[] = [
{ id: 1, name: "Alice Johnson", email: "alice@example.com", joined: "2024-05-01" },
{ id: 2, name: "Bob Smith", email: "bob@example.com", joined: "2024-05-03" },
{ id: 3, name: "Charlie Lee", email: "charlie@example.com", joined: "2024-05-04" },
{ id: 4, name: "Dana Miller", email: "dana@example.com", joined: "2024-05-06" },
{ id: 5, name: "Evan Brown", email: "evan@example.com", joined: "2024-05-09" },
{ id: 6, name: "Fay Green", email: "fay@example.com", joined: "2024-05-10" },
{ id: 7, name: "George King", email: "george@example.com", joined: "2024-05-11" },
{ id: 8, name: "Hannah Scott", email: "hannah@example.com", joined: "2024-05-12" },
{ id: 9, name: "Ian Clark", email: "ian@example.com", joined: "2024-05-13" },
{ id: 10, name: "Jane Doe", email: "jane@example.com", joined: "2024-05-14" },
];
export default function WithPagination() {
const perPage = 5;
const pageCount = Math.ceil(customers.length / perPage);
const [page, setPage] = useState(1);
const currentCustomers = customers.slice((page - 1) * perPage, page * perPage);
const goToPage = (p: number) => {
setPage(p);
};
return (
<div className="mx-auto w-full max-w-6xl rounded border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Joined</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentCustomers.map((customer) => (
<TableRow key={customer.id}>
<TableCell className="font-medium">{customer.name}</TableCell>
<TableCell>{customer.email}</TableCell>
<TableCell>{customer.joined}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Pagination className="py-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (page > 1) goToPage(page - 1);
}}
/>
</PaginationItem>
{Array.from({ length: pageCount }, (_, i) => (
<PaginationItem key={i}>
<PaginationLink
href="#"
isActive={page === i + 1}
onClick={(e) => {
e.preventDefault();
goToPage(i + 1);
}}
>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (page < pageCount) goToPage(page + 1);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
);
}