Tables
Tables with actions, filters, grouped rows, and a full data table.
Fromblocks.so
Table with Accordion
"use client"
import { ChevronDown, ChevronRight } from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
interface TableRowData {
id: string
name: string
category: string
value: number
date: string
children?: TableRowData[]
}
const sampleData: TableRowData[] = [
{
id: "001",
name: "Project Alpha",
category: "Development",
value: 45_000,
date: "2024-01-15",
children: [
{
id: "001-01",
name: "Frontend Module",
category: "Development",
value: 15_000,
date: "2024-01-16",
},
{
id: "001-02",
name: "Backend Module",
category: "Development",
value: 20_000,
date: "2024-01-21",
},
{
id: "001-03",
name: "Testing Suite",
category: "Development",
value: 10_000,
date: "2024-01-24",
},
],
},
{
id: "002",
name: "Marketing Campaign",
category: "Marketing",
value: 28_500,
date: "2024-01-18",
children: [
{
id: "002-01",
name: "Social Media",
category: "Marketing",
value: 12_000,
date: "2024-01-19",
},
{
id: "002-02",
name: "Email Marketing",
category: "Marketing",
value: 8500,
date: "2024-01-22",
},
{
id: "002-03",
name: "SEO Optimization",
category: "Marketing",
value: 8000,
date: "2024-01-23",
},
],
},
{
id: "003",
name: "Infrastructure Upgrade",
category: "Operations",
value: 67_200,
date: "2024-01-20",
children: [
{
id: "003-01",
name: "Cloud Migration",
category: "Operations",
value: 35_000,
date: "2024-01-21",
},
{
id: "003-02",
name: "Security Enhancement",
category: "Operations",
value: 32_200,
date: "2024-01-24",
},
],
},
{
id: "004",
name: "Customer Support",
category: "Service",
value: 19_800,
date: "2024-01-25",
},
]
interface AccordionRowProps {
row: TableRowData
defaultOpen?: boolean
}
function AccordionRow({ row, defaultOpen = false }: AccordionRowProps) {
const [isOpen, setIsOpen] = useState(defaultOpen)
const hasChildren = row.children && row.children.length > 0
return (
<Collapsible
onOpenChange={setIsOpen}
open={isOpen}
render={
<TableBody className="[&_tr:last-child]:border-b last:[&_tr:last-child]:border-0" />
}
>
<TableRow
className={cn(
"grid grid-cols-[40px_80px_180px_110px_100px_110px] bg-muted/50 hover:bg-muted/50",
isOpen && "border-b-0"
)}
>
<TableCell className="p-0">
<Button
aria-label={isOpen ? "Collapse row" : "Expand row"}
className={cn(
"h-full w-full rounded-none p-3 text-muted-foreground transition-colors",
hasChildren && "hover:bg-transparent hover:text-foreground",
!hasChildren && "cursor-default opacity-30"
)}
disabled={!hasChildren}
onClick={() => setIsOpen(!isOpen)}
size="icon"
variant="ghost"
>
{hasChildren ? (
isOpen ? (
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
) : (
<ChevronRight className="h-4 w-4 transition-transform duration-200" />
)
) : (
<div className="h-4 w-4" />
)}
</Button>
</TableCell>
<TableCell className="p-3 font-medium font-mono text-muted-foreground text-sm">
{row.id}
</TableCell>
<TableCell className="p-3 font-medium text-sm">{row.name}</TableCell>
<TableCell className="p-3 text-muted-foreground text-sm">
{row.category}
</TableCell>
<TableCell className="p-3 text-right font-mono font-semibold text-sm tabular-nums">
${row.value.toLocaleString()}
</TableCell>
<TableCell className="p-3 text-muted-foreground text-sm">
{row.date}
</TableCell>
</TableRow>
{hasChildren && (
<TableRow className="grid grid-cols-[40px_80px_180px_110px_100px_110px] border-b-0 hover:bg-transparent">
<TableCell className="col-span-6 p-0" colSpan={6}>
<CollapsibleContent>
<div className="w-full border-border border-b bg-muted/20">
<Table>
<TableHeader>
<TableRow className="grid grid-cols-[40px_80px_180px_110px_100px_110px] border-b-0 bg-muted/30">
<TableHead className="flex h-7 items-center border-border border-y px-3 py-1.5" />
<TableHead className="flex h-7 items-center border-border border-y px-3 py-1.5 text-xs">
ID
</TableHead>
<TableHead className="flex h-7 items-center border-border border-y px-3 py-1.5 text-xs">
Name
</TableHead>
<TableHead className="flex h-7 items-center border-border border-y px-3 py-1.5 text-xs">
Category
</TableHead>
<TableHead className="flex h-7 items-center justify-end border-border border-y px-3 py-1.5 text-right text-xs">
Value
</TableHead>
<TableHead className="flex h-7 items-center border-border border-y px-3 py-1.5 text-xs">
Date
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{row.children?.map((childRow) => (
<TableRow
className="grid grid-cols-[40px_80px_180px_110px_100px_110px]"
key={childRow.id}
>
<TableCell className="px-3 py-2" />
<TableCell className="px-3 py-2 font-mono text-muted-foreground text-xs tabular-nums">
{childRow.id}
</TableCell>
<TableCell className="px-3 py-2 font-medium text-xs">
{childRow.name}
</TableCell>
<TableCell className="px-3 py-2 text-muted-foreground text-xs">
{childRow.category}
</TableCell>
<TableCell className="px-3 py-2 text-right font-mono font-semibold text-xs tabular-nums">
${childRow.value.toLocaleString()}
</TableCell>
<TableCell className="px-3 py-2 text-muted-foreground text-xs">
{childRow.date}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CollapsibleContent>
</TableCell>
</TableRow>
)}
</Collapsible>
)
}
export default function Table01() {
return (
<div className="max-w-fit overflow-hidden rounded-lg border border-border bg-card shadow-sm">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="grid grid-cols-[40px_80px_180px_110px_100px_110px] bg-muted/50">
<TableHead className="p-3" />
<TableHead className="p-3 font-semibold text-foreground text-sm">
ID
</TableHead>
<TableHead className="p-3 font-semibold text-foreground text-sm">
Name
</TableHead>
<TableHead className="p-3 font-semibold text-foreground text-sm">
Category
</TableHead>
<TableHead className="p-3 text-right font-semibold text-foreground text-sm">
Value
</TableHead>
<TableHead className="p-3 font-semibold text-foreground text-sm">
Date
</TableHead>
</TableRow>
</TableHeader>
{sampleData.map((row, index) => (
<AccordionRow defaultOpen={index === 0} key={row.id} row={row} />
))}
</Table>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/table-01.jsonTable with Actions
"use client"
import {
CheckCircle,
FileTextIcon,
Loader2,
PauseIcon,
PlayIcon,
Trash2Icon,
} from "lucide-react"
import { useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
interface Task {
id: string
title: string
assignee: string
status: "pending" | "in-progress" | "completed" | "blocked"
priority: "low" | "medium" | "high" | "urgent"
dueDate: string
notes: string
}
type TaskActionType = "start" | "pause" | "complete" | "delete" | "view"
const tasks: Task[] = [
{
id: "TASK-001",
title: "Implement User Authentication",
assignee: "Sarah Chen",
status: "in-progress",
priority: "high",
dueDate: "2024-03-25",
notes: "OAuth 2.0 integration with Google and GitHub providers",
},
{
id: "TASK-002",
title: "Design Dashboard UI",
assignee: "Michael Torres",
status: "completed",
priority: "medium",
dueDate: "2024-03-20",
notes: "Finalize dashboard layout with responsive grid system",
},
{
id: "TASK-003",
title: "API Performance Optimization",
assignee: "Emma Rodriguez",
status: "pending",
priority: "urgent",
dueDate: "2024-03-22",
notes: "Reduce API response time by implementing caching strategy",
},
{
id: "TASK-004",
title: "Write Unit Tests",
assignee: "James Wilson",
status: "in-progress",
priority: "medium",
dueDate: "2024-03-28",
notes: "Achieve 80% code coverage for authentication module",
},
{
id: "TASK-005",
title: "Database Migration",
assignee: "Olivia Martinez",
status: "blocked",
priority: "high",
dueDate: "2024-03-24",
notes: "Waiting for infrastructure team approval before proceeding",
},
{
id: "TASK-006",
title: "Update Documentation",
assignee: "Lucas Anderson",
status: "pending",
priority: "low",
dueDate: "2024-03-30",
notes: "Document new API endpoints and authentication flow",
},
{
id: "TASK-007",
title: "Security Audit",
assignee: "Sophia Taylor",
status: "completed",
priority: "urgent",
dueDate: "2024-03-19",
notes:
"Conducted comprehensive security review and vulnerability assessment",
},
]
function getStatusBadge(status: Task["status"]) {
switch (status) {
case "pending":
return (
<Badge
className="border-0 bg-amber-500/15 text-amber-700 hover:bg-amber-500/25 dark:bg-amber-500/10 dark:text-amber-300 dark:hover:bg-amber-500/20"
variant="outline"
>
Pending
</Badge>
)
case "in-progress":
return (
<Badge
className="border-0 bg-blue-500/15 text-blue-700 hover:bg-blue-500/25 dark:bg-blue-500/10 dark:text-blue-400 dark:hover:bg-blue-500/20"
variant="outline"
>
In Progress
</Badge>
)
case "completed":
return (
<Badge
className="border-0 bg-green-500/15 text-green-700 hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:hover:bg-green-500/20"
variant="outline"
>
Completed
</Badge>
)
case "blocked":
return (
<Badge
className="border-0 bg-rose-500/15 text-rose-700 hover:bg-rose-500/25 dark:bg-rose-500/10 dark:text-rose-400 dark:hover:bg-rose-500/20"
variant="outline"
>
Blocked
</Badge>
)
default:
return <Badge variant="secondary">{status}</Badge>
}
}
export default function Table02() {
const [pendingAction, setPendingAction] = useState<{
id: string
type: TaskActionType
} | null>(null)
const isTaskActionPending = (action: TaskActionType, taskId: string) =>
pendingAction?.id === taskId && pendingAction.type === action
const isTaskBusy = (taskId: string) => pendingAction?.id === taskId
const handleAction = (task: Task, actionType: TaskActionType) => {
setPendingAction({ id: task.id, type: actionType })
setTimeout(() => {
setPendingAction(null)
console.log(`Action "${actionType}" completed for task:`, task.title)
}, 1000)
}
const renderTaskRow = (task: Task) => {
const busy = isTaskBusy(task.id)
const startPending = isTaskActionPending("start", task.id)
const pausePending = isTaskActionPending("pause", task.id)
const completePending = isTaskActionPending("complete", task.id)
const deletePending = isTaskActionPending("delete", task.id)
return (
<TableRow className="hover:bg-muted/50" key={task.id}>
<TableCell className="h-16 px-4 font-medium">{task.title}</TableCell>
<TableCell className="h-16 px-4 text-muted-foreground text-sm">
{task.assignee}
</TableCell>
<TableCell className="h-16 px-4">
{getStatusBadge(task.status)}
</TableCell>
<TableCell className="h-16 px-4 text-muted-foreground text-sm">
{task.dueDate}
</TableCell>
<TableCell className="h-16 max-w-[300px] px-4 text-muted-foreground text-sm">
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<span className="block cursor-help truncate" />}
>
{task.notes}
</TooltipTrigger>
<TooltipContent className="max-w-md">{task.notes}</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell className="h-16 px-4">
<TooltipProvider>
<div className="flex items-center gap-1">
{(task.status === "pending" || task.status === "blocked") && (
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Start"
className="h-8 w-8"
disabled={busy}
onClick={() => handleAction(task, "start")}
size="icon"
variant="outline"
/>
}
>
{startPending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<PlayIcon className="size-4" />
)}
</TooltipTrigger>
<TooltipContent>Start</TooltipContent>
</Tooltip>
)}
{task.status === "in-progress" && (
<>
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Pause"
className="h-8 w-8"
disabled={busy}
onClick={() => handleAction(task, "pause")}
size="icon"
variant="outline"
/>
}
>
{pausePending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<PauseIcon className="size-4" />
)}
</TooltipTrigger>
<TooltipContent>Pause</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Complete"
className="h-8 w-8"
disabled={busy}
onClick={() => handleAction(task, "complete")}
size="icon"
variant="outline"
/>
}
>
{completePending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<CheckCircle className="size-4" />
)}
</TooltipTrigger>
<TooltipContent>Complete</TooltipContent>
</Tooltip>
</>
)}
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Delete"
className="h-8 w-8 text-destructive hover:bg-destructive hover:text-white"
disabled={busy}
onClick={() => handleAction(task, "delete")}
size="icon"
variant="outline"
/>
}
>
{deletePending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Trash2Icon className="size-4" />
)}
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="View details"
className="h-8 w-8"
disabled={busy}
onClick={() => handleAction(task, "view")}
size="icon"
variant="outline"
/>
}
>
<FileTextIcon className="size-4" />
</TooltipTrigger>
<TooltipContent>View Details</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</TableCell>
</TableRow>
)
}
return (
<div className="w-[95%] rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow className="border-b hover:bg-transparent">
<TableHead className="h-12 px-4 font-medium">Title</TableHead>
<TableHead className="h-12 px-4 font-medium">Assignee</TableHead>
<TableHead className="h-12 w-[120px] px-4 font-medium">
Status
</TableHead>
<TableHead className="h-12 px-4 font-medium">Due Date</TableHead>
<TableHead className="h-12 px-4 font-medium">Notes</TableHead>
<TableHead className="h-12 w-[180px] px-4 font-medium">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>{tasks.map(renderTaskRow)}</TableBody>
</Table>
</div>
)
}npx shadcn@latest add https://blocks.so/r/table-02.jsonTable with Filters
"use client"
import { useState } from "react"
import { Badge } from "@/components/ui/badge"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
type Status = "active" | "pending" | "discontinued" | "on-hold"
interface Product {
sku: string
productName: string
stockLevel: number
category: string
status: Status
unitPrice: string
lastRestocked: string
}
const data: Product[] = [
{
sku: "SKU-8472",
productName: "Wireless Mouse Pro",
stockLevel: 245,
category: "Electronics",
status: "active",
unitPrice: "$24.99",
lastRestocked: "Oct 15, 2024",
},
{
sku: "SKU-3391",
productName: "Ergonomic Keyboard",
stockLevel: 89,
category: "Electronics",
status: "active",
unitPrice: "$79.99",
lastRestocked: "Oct 18, 2024",
},
{
sku: "SKU-7156",
productName: "Office Chair Deluxe",
stockLevel: 12,
category: "Furniture",
status: "pending",
unitPrice: "$299.99",
lastRestocked: "Oct 12, 2024",
},
{
sku: "SKU-9204",
productName: "USB-C Hub Adapter",
stockLevel: 456,
category: "Accessories",
status: "active",
unitPrice: "$34.50",
lastRestocked: "Oct 19, 2024",
},
{
sku: "SKU-1638",
productName: "Standing Desk Frame",
stockLevel: 5,
category: "Furniture",
status: "on-hold",
unitPrice: "$449.00",
lastRestocked: "Oct 10, 2024",
},
{
sku: "SKU-5529",
productName: "Laptop Stand Aluminum",
stockLevel: 178,
category: "Accessories",
status: "active",
unitPrice: "$45.99",
lastRestocked: "Oct 17, 2024",
},
{
sku: "SKU-4817",
productName: "Mechanical Keyboard RGB",
stockLevel: 0,
category: "Electronics",
status: "discontinued",
unitPrice: "$129.99",
lastRestocked: "Oct 05, 2024",
},
]
function getStatusBadge(status: Status) {
switch (status) {
case "active":
return (
<Badge
className="border-0 bg-green-500/15 text-green-700 hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:hover:bg-green-500/20"
variant="outline"
>
Active
</Badge>
)
case "pending":
return (
<Badge
className="border-0 bg-amber-500/15 text-amber-700 hover:bg-amber-500/25 dark:bg-amber-500/10 dark:text-amber-300 dark:hover:bg-amber-500/20"
variant="outline"
>
Pending
</Badge>
)
case "discontinued":
return (
<Badge
className="border-0 bg-rose-500/15 text-rose-700 hover:bg-rose-500/25 dark:bg-rose-500/10 dark:text-rose-400 dark:hover:bg-rose-500/20"
variant="outline"
>
Discontinued
</Badge>
)
case "on-hold":
return (
<Badge
className="border-0 bg-blue-500/15 text-blue-700 hover:bg-blue-500/25 dark:bg-blue-500/10 dark:text-blue-400 dark:hover:bg-blue-500/20"
variant="outline"
>
On Hold
</Badge>
)
default:
return null
}
}
export default function Table03() {
const [selectedCategory, setSelectedCategory] = useState<string>("all")
const filteredData = data.filter((item) => {
const categoryMatch =
selectedCategory === "all" || item.category === selectedCategory
return categoryMatch
})
const uniqueCategories = Array.from(
new Set(data.map((item) => item.category))
)
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h3 className="text-balance font-semibold text-foreground text-lg">
Product Inventory
</h3>
<p className="mt-1 text-pretty text-muted-foreground text-sm">
Track and manage product stock levels across all warehouse
locations.
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select
items={[
{ value: "all", label: "All Categories" },
...uniqueCategories.map((c) => ({ value: c, label: c })),
]}
onValueChange={(value) => {
if (value) {
setSelectedCategory(value)
}
}}
value={selectedCategory}
>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="Filter by category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{uniqueCategories.map((category) => (
<SelectItem key={category} value={category}>
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow className="border-b hover:bg-transparent">
<TableHead className="h-12 px-4 font-medium">SKU</TableHead>
<TableHead className="h-12 px-4 font-medium">
Product Name
</TableHead>
<TableHead className="h-12 px-4 font-medium">Category</TableHead>
<TableHead className="h-12 px-4 font-medium">Status</TableHead>
<TableHead className="h-12 px-4 text-right font-medium">
Unit Price
</TableHead>
<TableHead className="h-12 px-4 text-right font-medium">
Last Restocked
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableRow className="hover:bg-muted/50" key={item.sku}>
<TableCell className="h-14 px-4 font-medium font-mono text-sm tabular-nums">
{item.sku}
</TableCell>
<TableCell className="h-14 px-4 font-medium">
{item.productName}
</TableCell>
<TableCell className="h-14 px-4 text-muted-foreground text-sm">
{item.category}
</TableCell>
<TableCell className="h-14 px-4">
{getStatusBadge(item.status)}
</TableCell>
<TableCell className="h-14 px-4 text-right font-mono font-semibold text-sm tabular-nums">
{item.unitPrice}
</TableCell>
<TableCell className="h-14 px-4 text-right text-muted-foreground text-sm">
{item.lastRestocked}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
className="h-24 text-center text-muted-foreground"
colSpan={6}
>
No products found matching the selected filters.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/table-03.jsonTable with Grouped Rows
"use client"
import { Fragment } from "react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
interface AssignedPerson {
name: string
initials: string
}
interface TaskItem {
id: string
task: string
budget: string
deadline: string
assigned: AssignedPerson[]
status: string
}
interface TaskGroup {
name: string
items: TaskItem[]
}
type StatusVariant = "success" | "warning" | "default" | "secondary"
const avatarColors = [
"bg-blue-500",
"bg-purple-500",
"bg-emerald-500",
"bg-cyan-500",
"bg-rose-500",
"bg-indigo-500",
]
function getAvatarColor(initials: string) {
const seed = initials
.split("")
.reduce((acc, char) => acc + char.charCodeAt(0), 0)
return avatarColors[seed % avatarColors.length]
}
function AvatarStack({ people }: { people: AssignedPerson[] }) {
return (
<div className="-space-x-2 flex">
{people.map((person, index) => (
<Avatar
className={cn(
"h-6 w-6 border-2 border-background text-[10px]",
getAvatarColor(person.initials)
)}
key={index}
>
<AvatarFallback
className={cn(
"font-medium text-white",
getAvatarColor(person.initials)
)}
>
{person.initials}
</AvatarFallback>
</Avatar>
))}
</div>
)
}
const statusStyles: Record<StatusVariant, { badge: string; dot: string }> = {
success: {
badge:
"bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300",
dot: "bg-emerald-500",
},
warning: {
badge: "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",
dot: "bg-amber-500",
},
default: {
badge: "bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
dot: "bg-blue-500",
},
secondary: {
badge: "bg-muted text-muted-foreground",
dot: "bg-muted-foreground",
},
}
function StatusBadge({
status,
variant = "default",
}: {
status: string
variant?: StatusVariant
}) {
const styles = statusStyles[variant]
return (
<Badge
className={cn("gap-1.5 rounded-full", styles.badge)}
variant="outline"
>
<span
aria-hidden="true"
className={cn("size-1.5 rounded-full", styles.dot)}
/>
{status}
</Badge>
)
}
const data: TaskGroup[] = [
{
name: "Engineering",
items: [
{
id: "1",
task: "API Integration Overhaul",
budget: "$32,000",
deadline: "Dec 15, 2024",
assigned: [
{ name: "Marcus Chen", initials: "MC" },
{ name: "Priya Sharma", initials: "PS" },
],
status: "In Progress",
},
{
id: "2",
task: "Database Migration",
budget: "$18,500",
deadline: "Jan 20, 2025",
assigned: [{ name: "David Kim", initials: "DK" }],
status: "Completed",
},
{
id: "3",
task: "Mobile App Redesign",
budget: "$55,000",
deadline: "Feb 28, 2025",
assigned: [
{ name: "Lena Rodriguez", initials: "LR" },
{ name: "Tom Anderson", initials: "TA" },
{ name: "Nina Patel", initials: "NP" },
],
status: "Planning",
},
],
},
{
name: "Marketing",
items: [
{
id: "4",
task: "Q1 Campaign Launch",
budget: "$24,000",
deadline: "Jan 5, 2025",
assigned: [
{ name: "Rachel Green", initials: "RG" },
{ name: "Chris Evans", initials: "CE" },
],
status: "In Progress",
},
{
id: "5",
task: "Brand Refresh Project",
budget: "$42,000",
deadline: "Mar 1, 2025",
assigned: [{ name: "Monica Geller", initials: "MG" }],
status: "Planning",
},
],
},
{
name: "Operations",
items: [
{
id: "6",
task: "Vendor Contract Review",
budget: "$8,000",
deadline: "Dec 30, 2024",
assigned: [
{ name: "Joe Tribbiani", initials: "JT" },
{ name: "Phoebe Buffay", initials: "PB" },
],
status: "Completed",
},
{
id: "7",
task: "Office Expansion Setup",
budget: "$120,000",
deadline: "Apr 15, 2025",
assigned: [{ name: "Ross Geller", initials: "RG" }],
status: "On Hold",
},
],
},
]
function getStatusVariant(status: string): StatusVariant {
switch (status) {
case "Completed":
return "success"
case "In Progress":
return "default"
case "Planning":
return "secondary"
case "On Hold":
return "warning"
default:
return "default"
}
}
export default function Table04() {
return (
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-48 font-medium">Task</TableHead>
<TableHead className="font-medium">Budget</TableHead>
<TableHead className="font-medium">Deadline</TableHead>
<TableHead className="font-medium">Assigned</TableHead>
<TableHead className="w-28 font-medium">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((group) => (
<Fragment key={group.name}>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableCell className="py-2 font-semibold" colSpan={5}>
{group.name}
<span className="ml-2 font-normal text-muted-foreground">
{group.items.length}
</span>
</TableCell>
</TableRow>
{group.items.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.task}</TableCell>
<TableCell>{item.budget}</TableCell>
<TableCell>{item.deadline}</TableCell>
<TableCell>
<AvatarStack people={item.assigned} />
</TableCell>
<TableCell>
<StatusBadge
status={item.status}
variant={getStatusVariant(item.status)}
/>
</TableCell>
</TableRow>
))}
</Fragment>
))}
</TableBody>
</Table>
</div>
)
}npx shadcn@latest add https://blocks.so/r/table-04.jsonData Table
"use client"
import {
columnFilteringFeature,
columnVisibilityFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
FlexRender,
globalFilteringFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
useTable,
} from "@tanstack/react-table"
import {
ChevronLeft,
ChevronRight,
Eye,
MoreHorizontal,
Pencil,
Trash2,
} from "lucide-react"
import { useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
type Status = "completed" | "pending" | "processing" | "cancelled"
interface Item {
id: string
name: string
date: string
status: Status
amount: string
}
const statusConfig: Record<Status, { label: string; className: string }> = {
completed: {
label: "Completed",
className:
"bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400",
},
pending: {
label: "Pending",
className:
"bg-amber-500/15 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
},
processing: {
label: "Processing",
className:
"bg-blue-500/15 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400",
},
cancelled: {
label: "Cancelled",
className:
"bg-rose-500/15 text-rose-700 dark:bg-rose-500/10 dark:text-rose-400",
},
}
function StatusBadge({ status }: { status: Status }) {
const config = statusConfig[status]
return (
<Badge className={cn("border-0", config.className)} variant="outline">
{config.label}
</Badge>
)
}
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
globalFilteringFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
})
const columnHelper = createColumnHelper<typeof features, Item>()
const columns = columnHelper.columns([
columnHelper.display({
id: "select",
header: ({ table }) => (
<Checkbox
aria-label="Select all"
checked={table.getIsAllPageRowsSelected()}
indeterminate={
table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
cell: ({ row }) => (
<Checkbox
aria-label="Select row"
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
enableSorting: false,
enableHiding: false,
}),
columnHelper.accessor("name", {
header: "Name",
cell: ({ row }) => (
<span className="font-medium">{row.getValue("name")}</span>
),
}),
columnHelper.accessor("date", {
header: "Date",
}),
columnHelper.accessor("status", {
header: "Status",
cell: ({ row }) => <StatusBadge status={row.getValue("status")} />,
}),
columnHelper.accessor("amount", {
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => (
<div className="text-right font-medium">{row.getValue("amount")}</div>
),
}),
columnHelper.display({
id: "actions",
cell: () => (
<div className="text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button className="h-8 w-8" size="icon" variant="ghost" />}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Eye className="mr-2 h-4 w-4" />
View details
</DropdownMenuItem>
<DropdownMenuItem>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
}),
])
const data: Item[] = [
{
id: "1",
name: "Project Alpha",
date: "Jan 15, 2024",
status: "completed",
amount: "$2,500",
},
{
id: "2",
name: "Website Redesign",
date: "Feb 3, 2024",
status: "processing",
amount: "$4,200",
},
{
id: "3",
name: "Mobile App MVP",
date: "Feb 18, 2024",
status: "pending",
amount: "$8,750",
},
{
id: "4",
name: "Brand Identity",
date: "Mar 5, 2024",
status: "completed",
amount: "$1,800",
},
{
id: "5",
name: "Marketing Campaign",
date: "Mar 22, 2024",
status: "cancelled",
amount: "$3,400",
},
{
id: "6",
name: "Analytics Dashboard",
date: "Apr 8, 2024",
status: "processing",
amount: "$5,600",
},
{
id: "7",
name: "E-commerce Platform",
date: "Apr 25, 2024",
status: "pending",
amount: "$12,000",
},
{
id: "8",
name: "API Integration",
date: "May 10, 2024",
status: "completed",
amount: "$3,200",
},
]
export default function Table05() {
const [globalFilter, setGlobalFilter] = useState("")
const table = useTable({
features,
data,
columns,
onGlobalFilterChange: setGlobalFilter,
state: {
globalFilter,
},
initialState: {
pagination: { pageIndex: 0, pageSize: 5 },
},
})
const pageCount = table.getPageCount()
const currentPage = table.state.pagination.pageIndex + 1
return (
<div className="w-full max-w-3xl space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm">Show</span>
<Select
onValueChange={(value) => table.setPageSize(Number(value))}
value={String(table.state.pagination.pageSize)}
>
<SelectTrigger className="h-8 w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[5, 10, 20].map((size) => (
<SelectItem key={size} value={String(size)}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-muted-foreground text-sm">entries</span>
</div>
<Input
className="h-8 w-full sm:w-64"
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search..."
value={globalFilter}
/>
</div>
<div className="rounded-lg border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<FlexRender header={header} />
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
data-state={row.getIsSelected() && "selected"}
key={row.id}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<FlexRender cell={cell} />
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
className="h-24 text-center"
colSpan={columns.length}
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-pretty text-muted-foreground text-sm">
Showing{" "}
{table.state.pagination.pageIndex * table.state.pagination.pageSize +
1}{" "}
to{" "}
{Math.min(
(table.state.pagination.pageIndex + 1) *
table.state.pagination.pageSize,
table.getFilteredRowModel().rows.length
)}{" "}
of {table.getFilteredRowModel().rows.length} entries
</p>
<div className="flex items-center gap-1">
<Button
aria-label="Previous page"
className="h-8 w-8"
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
size="icon"
variant="outline"
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous page</span>
</Button>
{Array.from({ length: pageCount }, (_, i) => i + 1).map((page) => (
<Button
aria-label={`Go to page ${page}`}
className="h-8 w-8"
key={page}
onClick={() => table.setPageIndex(page - 1)}
size="icon"
variant={currentPage === page ? "default" : "outline"}
>
{page}
</Button>
))}
<Button
aria-label="Next page"
className="h-8 w-8"
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
size="icon"
variant="outline"
>
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next page</span>
</Button>
</div>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/table-05.json