Components
Loading preview...
A responsive table component.
npx shadcn@latest add https://21st.dev/r/shadcn/table"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, Trash2 } from "lucide-react"
import { useMemo, useState } from "react"
interface Tag {
name: string
bookmarks: number
description: string
relatedTags: string[]
}
interface SortState {
key: keyof Tag
order: "asc" | "desc"
}
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 my-6 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" size="sm">
<ArrowUp className="size-4 text-muted-foreground" />
<span className="ml-2">Sort by</span>
</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-3.5" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="size-3.5" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
export { WithSort }