Quellcode durchsuchen

feat: implement tasks table and page

satnaing vor 2 Jahren
Ursprung
Commit
cbcfd9ee36

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "@radix-ui/react-toast": "^1.1.5",
     "@radix-ui/react-tooltip": "^1.0.7",
     "@tabler/icons-react": "^2.46.0",
+    "@tanstack/react-table": "^8.11.8",
     "class-variance-authority": "^0.7.0",
     "clsx": "^2.1.0",
     "cmdk": "^0.2.1",

+ 20 - 0
pnpm-lock.yaml

@@ -59,6 +59,9 @@ dependencies:
   '@tabler/icons-react':
     specifier: ^2.46.0
     version: 2.46.0(react@18.2.0)
+  '@tanstack/react-table':
+    specifier: ^8.11.8
+    version: 8.11.8(react-dom@18.2.0)(react@18.2.0)
   class-variance-authority:
     specifier: ^0.7.0
     version: 0.7.0
@@ -1858,6 +1861,23 @@ packages:
     resolution: {integrity: sha512-Q5G8Pj5IO+Uhc6pszpu5/hGYY018JwEzzvmuqr+gKJtfIvAHA3umpwUilMRLEy89p+WCP+YsDhicMhfBCCv1qA==}
     dev: false
 
+  /@tanstack/react-table@8.11.8(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-NEwvIq4iSiDQozEyvbdiSdCOiLa+g5xHmdEnvwDb98FObcK6YkBOkRrs/CNqrKdDy+/lqoIllIWHk+M80GW6+g==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      react: '>=16'
+      react-dom: '>=16'
+    dependencies:
+      '@tanstack/table-core': 8.11.8
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /@tanstack/table-core@8.11.8:
+    resolution: {integrity: sha512-DECHvtq4YW4U/gqg6etup7ydt/RB1Bi1pJaMpHUXl65ooW1d71Nv7BzD66rUdHrBSNdyiW3PLTPUQlpXjAgDeA==}
+    engines: {node: '>=12'}
+    dev: false
+
   /@types/d3-array@3.2.1:
     resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
     dev: false

+ 36 - 0
src/components/ui/badge.tsx

@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+  {
+    variants: {
+      variant: {
+        default:
+          "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+        secondary:
+          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+        destructive:
+          "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+        outline: "text-foreground",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+)
+
+export interface BadgeProps
+  extends React.HTMLAttributes<HTMLDivElement>,
+    VariantProps<typeof badgeVariants> {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+  return (
+    <div className={cn(badgeVariants({ variant }), className)} {...props} />
+  )
+}
+
+export { Badge, badgeVariants }

+ 120 - 0
src/components/ui/table.tsx

@@ -0,0 +1,120 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Table = React.forwardRef<
+  HTMLTableElement,
+  React.HTMLAttributes<HTMLTableElement>
+>(({ className, ...props }, ref) => (
+  <div className="relative w-full overflow-auto">
+    <table
+      ref={ref}
+      className={cn("w-full caption-bottom text-sm", className)}
+      {...props}
+    />
+  </div>
+))
+Table.displayName = "Table"
+
+const TableHeader = React.forwardRef<
+  HTMLTableSectionElement,
+  React.HTMLAttributes<HTMLTableSectionElement>
+>(({ className, ...props }, ref) => (
+  <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
+))
+TableHeader.displayName = "TableHeader"
+
+const TableBody = React.forwardRef<
+  HTMLTableSectionElement,
+  React.HTMLAttributes<HTMLTableSectionElement>
+>(({ className, ...props }, ref) => (
+  <tbody
+    ref={ref}
+    className={cn("[&_tr:last-child]:border-0", className)}
+    {...props}
+  />
+))
+TableBody.displayName = "TableBody"
+
+const TableFooter = React.forwardRef<
+  HTMLTableSectionElement,
+  React.HTMLAttributes<HTMLTableSectionElement>
+>(({ className, ...props }, ref) => (
+  <tfoot
+    ref={ref}
+    className={cn(
+      "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
+      className
+    )}
+    {...props}
+  />
+))
+TableFooter.displayName = "TableFooter"
+
+const TableRow = React.forwardRef<
+  HTMLTableRowElement,
+  React.HTMLAttributes<HTMLTableRowElement>
+>(({ className, ...props }, ref) => (
+  <tr
+    ref={ref}
+    className={cn(
+      "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
+      className
+    )}
+    {...props}
+  />
+))
+TableRow.displayName = "TableRow"
+
+const TableHead = React.forwardRef<
+  HTMLTableCellElement,
+  React.ThHTMLAttributes<HTMLTableCellElement>
+>(({ className, ...props }, ref) => (
+  <th
+    ref={ref}
+    className={cn(
+      "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+      className
+    )}
+    {...props}
+  />
+))
+TableHead.displayName = "TableHead"
+
+const TableCell = React.forwardRef<
+  HTMLTableCellElement,
+  React.TdHTMLAttributes<HTMLTableCellElement>
+>(({ className, ...props }, ref) => (
+  <td
+    ref={ref}
+    className={cn(
+      "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+      className
+    )}
+    {...props}
+  />
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+  HTMLTableCaptionElement,
+  React.HTMLAttributes<HTMLTableCaptionElement>
+>(({ className, ...props }, ref) => (
+  <caption
+    ref={ref}
+    className={cn("mt-4 text-sm text-muted-foreground", className)}
+    {...props}
+  />
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+  Table,
+  TableHeader,
+  TableBody,
+  TableFooter,
+  TableHead,
+  TableRow,
+  TableCell,
+  TableCaption,
+}

+ 7 - 0
src/data/sidelinks.tsx

@@ -2,6 +2,7 @@ import {
   IconBarrierBlock,
   IconBoxSeam,
   IconChartHistogram,
+  IconChecklist,
   IconError404,
   IconExclamationCircle,
   IconHexagonNumber1,
@@ -36,6 +37,12 @@ export const sidelinks: SideLink[] = [
     href: '/',
     icon: <IconLayoutDashboard size={18} />,
   },
+  {
+    title: 'Tasks',
+    label: '3',
+    href: '/tasks',
+    icon: <IconChecklist size={18} />,
+  },
   {
     title: 'Chats',
     label: '9',

+ 121 - 0
src/pages/tasks/components/columns.tsx

@@ -0,0 +1,121 @@
+import { ColumnDef } from '@tanstack/react-table'
+
+import { Badge } from '@/components/ui/badge'
+import { Checkbox } from '@/components/ui/checkbox'
+import { DataTableColumnHeader } from './data-table-column-header'
+import { DataTableRowActions } from './data-table-row-actions'
+
+import { labels, priorities, statuses } from '../data/data'
+import { Task } from '../data/schema'
+
+export const columns: ColumnDef<Task>[] = [
+  {
+    id: 'select',
+    header: ({ table }) => (
+      <Checkbox
+        checked={
+          table.getIsAllPageRowsSelected() ||
+          (table.getIsSomePageRowsSelected() && 'indeterminate')
+        }
+        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+        aria-label='Select all'
+        className='translate-y-[2px]'
+      />
+    ),
+    cell: ({ row }) => (
+      <Checkbox
+        checked={row.getIsSelected()}
+        onCheckedChange={(value) => row.toggleSelected(!!value)}
+        aria-label='Select row'
+        className='translate-y-[2px]'
+      />
+    ),
+    enableSorting: false,
+    enableHiding: false,
+  },
+  {
+    accessorKey: 'id',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='Task' />
+    ),
+    cell: ({ row }) => <div className='w-[80px]'>{row.getValue('id')}</div>,
+    enableSorting: false,
+    enableHiding: false,
+  },
+  {
+    accessorKey: 'title',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='Title' />
+    ),
+    cell: ({ row }) => {
+      const label = labels.find((label) => label.value === row.original.label)
+
+      return (
+        <div className='flex space-x-2'>
+          {label && <Badge variant='outline'>{label.label}</Badge>}
+          <span className='max-w-32 truncate font-medium sm:max-w-72 md:max-w-[31rem]'>
+            {row.getValue('title')}
+          </span>
+        </div>
+      )
+    },
+  },
+  {
+    accessorKey: 'status',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='Status' />
+    ),
+    cell: ({ row }) => {
+      const status = statuses.find(
+        (status) => status.value === row.getValue('status')
+      )
+
+      if (!status) {
+        return null
+      }
+
+      return (
+        <div className='flex w-[100px] items-center'>
+          {status.icon && (
+            <status.icon className='mr-2 h-4 w-4 text-muted-foreground' />
+          )}
+          <span>{status.label}</span>
+        </div>
+      )
+    },
+    filterFn: (row, id, value) => {
+      return value.includes(row.getValue(id))
+    },
+  },
+  {
+    accessorKey: 'priority',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='Priority' />
+    ),
+    cell: ({ row }) => {
+      const priority = priorities.find(
+        (priority) => priority.value === row.getValue('priority')
+      )
+
+      if (!priority) {
+        return null
+      }
+
+      return (
+        <div className='flex items-center'>
+          {priority.icon && (
+            <priority.icon className='mr-2 h-4 w-4 text-muted-foreground' />
+          )}
+          <span>{priority.label}</span>
+        </div>
+      )
+    },
+    filterFn: (row, id, value) => {
+      return value.includes(row.getValue(id))
+    },
+  },
+  {
+    id: 'actions',
+    cell: ({ row }) => <DataTableRowActions row={row} />,
+  },
+]

+ 71 - 0
src/pages/tasks/components/data-table-column-header.tsx

@@ -0,0 +1,71 @@
+import {
+  ArrowDownIcon,
+  ArrowUpIcon,
+  CaretSortIcon,
+  EyeNoneIcon,
+} from '@radix-ui/react-icons'
+import { Column } from '@tanstack/react-table'
+
+import { Button } from '@/components/ui/button'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+import { cn } from '@/lib/utils'
+
+interface DataTableColumnHeaderProps<TData, TValue>
+  extends React.HTMLAttributes<HTMLDivElement> {
+  column: Column<TData, TValue>
+  title: string
+}
+
+export function DataTableColumnHeader<TData, TValue>({
+  column,
+  title,
+  className,
+}: DataTableColumnHeaderProps<TData, TValue>) {
+  if (!column.getCanSort()) {
+    return <div className={cn(className)}>{title}</div>
+  }
+
+  return (
+    <div className={cn('flex items-center space-x-2', className)}>
+      <DropdownMenu>
+        <DropdownMenuTrigger asChild>
+          <Button
+            variant='ghost'
+            size='sm'
+            className='-ml-3 h-8 data-[state=open]:bg-accent'
+          >
+            <span>{title}</span>
+            {column.getIsSorted() === 'desc' ? (
+              <ArrowDownIcon className='ml-2 h-4 w-4' />
+            ) : column.getIsSorted() === 'asc' ? (
+              <ArrowUpIcon className='ml-2 h-4 w-4' />
+            ) : (
+              <CaretSortIcon className='ml-2 h-4 w-4' />
+            )}
+          </Button>
+        </DropdownMenuTrigger>
+        <DropdownMenuContent align='start'>
+          <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
+            <ArrowUpIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
+            Asc
+          </DropdownMenuItem>
+          <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
+            <ArrowDownIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
+            Desc
+          </DropdownMenuItem>
+          <DropdownMenuSeparator />
+          <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
+            <EyeNoneIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
+            Hide
+          </DropdownMenuItem>
+        </DropdownMenuContent>
+      </DropdownMenu>
+    </div>
+  )
+}

+ 147 - 0
src/pages/tasks/components/data-table-faceted-filter.tsx

@@ -0,0 +1,147 @@
+import * as React from 'react'
+import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'
+import { Column } from '@tanstack/react-table'
+
+import { cn } from '@/lib/utils'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+} from '@/components/ui/command'
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from '@/components/ui/popover'
+import { Separator } from '@/components/ui/separator'
+
+interface DataTableFacetedFilterProps<TData, TValue> {
+  column?: Column<TData, TValue>
+  title?: string
+  options: {
+    label: string
+    value: string
+    icon?: React.ComponentType<{ className?: string }>
+  }[]
+}
+
+export function DataTableFacetedFilter<TData, TValue>({
+  column,
+  title,
+  options,
+}: DataTableFacetedFilterProps<TData, TValue>) {
+  const facets = column?.getFacetedUniqueValues()
+  const selectedValues = new Set(column?.getFilterValue() as string[])
+
+  return (
+    <Popover>
+      <PopoverTrigger asChild>
+        <Button variant='outline' size='sm' className='h-8 border-dashed'>
+          <PlusCircledIcon className='mr-2 h-4 w-4' />
+          {title}
+          {selectedValues?.size > 0 && (
+            <>
+              <Separator orientation='vertical' className='mx-2 h-4' />
+              <Badge
+                variant='secondary'
+                className='rounded-sm px-1 font-normal lg:hidden'
+              >
+                {selectedValues.size}
+              </Badge>
+              <div className='hidden space-x-1 lg:flex'>
+                {selectedValues.size > 2 ? (
+                  <Badge
+                    variant='secondary'
+                    className='rounded-sm px-1 font-normal'
+                  >
+                    {selectedValues.size} selected
+                  </Badge>
+                ) : (
+                  options
+                    .filter((option) => selectedValues.has(option.value))
+                    .map((option) => (
+                      <Badge
+                        variant='secondary'
+                        key={option.value}
+                        className='rounded-sm px-1 font-normal'
+                      >
+                        {option.label}
+                      </Badge>
+                    ))
+                )}
+              </div>
+            </>
+          )}
+        </Button>
+      </PopoverTrigger>
+      <PopoverContent className='w-[200px] p-0' align='start'>
+        <Command>
+          <CommandInput placeholder={title} />
+          <CommandList>
+            <CommandEmpty>No results found.</CommandEmpty>
+            <CommandGroup>
+              {options.map((option) => {
+                const isSelected = selectedValues.has(option.value)
+                return (
+                  <CommandItem
+                    key={option.value}
+                    onSelect={() => {
+                      if (isSelected) {
+                        selectedValues.delete(option.value)
+                      } else {
+                        selectedValues.add(option.value)
+                      }
+                      const filterValues = Array.from(selectedValues)
+                      column?.setFilterValue(
+                        filterValues.length ? filterValues : undefined
+                      )
+                    }}
+                  >
+                    <div
+                      className={cn(
+                        'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
+                        isSelected
+                          ? 'bg-primary text-primary-foreground'
+                          : 'opacity-50 [&_svg]:invisible'
+                      )}
+                    >
+                      <CheckIcon className={cn('h-4 w-4')} />
+                    </div>
+                    {option.icon && (
+                      <option.icon className='mr-2 h-4 w-4 text-muted-foreground' />
+                    )}
+                    <span>{option.label}</span>
+                    {facets?.get(option.value) && (
+                      <span className='ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs'>
+                        {facets.get(option.value)}
+                      </span>
+                    )}
+                  </CommandItem>
+                )
+              })}
+            </CommandGroup>
+            {selectedValues.size > 0 && (
+              <>
+                <CommandSeparator />
+                <CommandGroup>
+                  <CommandItem
+                    onSelect={() => column?.setFilterValue(undefined)}
+                    className='justify-center text-center'
+                  >
+                    Clear filters
+                  </CommandItem>
+                </CommandGroup>
+              </>
+            )}
+          </CommandList>
+        </Command>
+      </PopoverContent>
+    </Popover>
+  )
+}

+ 97 - 0
src/pages/tasks/components/data-table-pagination.tsx

@@ -0,0 +1,97 @@
+import {
+  ChevronLeftIcon,
+  ChevronRightIcon,
+  DoubleArrowLeftIcon,
+  DoubleArrowRightIcon,
+} from '@radix-ui/react-icons'
+import { Table } from '@tanstack/react-table'
+
+import { Button } from '@/components/ui/button'
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'
+
+interface DataTablePaginationProps<TData> {
+  table: Table<TData>
+}
+
+export function DataTablePagination<TData>({
+  table,
+}: DataTablePaginationProps<TData>) {
+  return (
+    <div className='flex items-center justify-between overflow-auto px-2'>
+      <div className='hidden flex-1 text-sm text-muted-foreground sm:block'>
+        {table.getFilteredSelectedRowModel().rows.length} of{' '}
+        {table.getFilteredRowModel().rows.length} row(s) selected.
+      </div>
+      <div className='flex items-center sm:space-x-6 lg:space-x-8'>
+        <div className='flex items-center space-x-2'>
+          <p className='hidden text-sm font-medium sm:block'>Rows per page</p>
+          <Select
+            value={`${table.getState().pagination.pageSize}`}
+            onValueChange={(value) => {
+              table.setPageSize(Number(value))
+            }}
+          >
+            <SelectTrigger className='h-8 w-[70px]'>
+              <SelectValue placeholder={table.getState().pagination.pageSize} />
+            </SelectTrigger>
+            <SelectContent side='top'>
+              {[10, 20, 30, 40, 50].map((pageSize) => (
+                <SelectItem key={pageSize} value={`${pageSize}`}>
+                  {pageSize}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+        <div className='flex w-[100px] items-center justify-center text-sm font-medium'>
+          Page {table.getState().pagination.pageIndex + 1} of{' '}
+          {table.getPageCount()}
+        </div>
+        <div className='flex items-center space-x-2'>
+          <Button
+            variant='outline'
+            className='hidden h-8 w-8 p-0 lg:flex'
+            onClick={() => table.setPageIndex(0)}
+            disabled={!table.getCanPreviousPage()}
+          >
+            <span className='sr-only'>Go to first page</span>
+            <DoubleArrowLeftIcon className='h-4 w-4' />
+          </Button>
+          <Button
+            variant='outline'
+            className='h-8 w-8 p-0'
+            onClick={() => table.previousPage()}
+            disabled={!table.getCanPreviousPage()}
+          >
+            <span className='sr-only'>Go to previous page</span>
+            <ChevronLeftIcon className='h-4 w-4' />
+          </Button>
+          <Button
+            variant='outline'
+            className='h-8 w-8 p-0'
+            onClick={() => table.nextPage()}
+            disabled={!table.getCanNextPage()}
+          >
+            <span className='sr-only'>Go to next page</span>
+            <ChevronRightIcon className='h-4 w-4' />
+          </Button>
+          <Button
+            variant='outline'
+            className='hidden h-8 w-8 p-0 lg:flex'
+            onClick={() => table.setPageIndex(table.getPageCount() - 1)}
+            disabled={!table.getCanNextPage()}
+          >
+            <span className='sr-only'>Go to last page</span>
+            <DoubleArrowRightIcon className='h-4 w-4' />
+          </Button>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 67 - 0
src/pages/tasks/components/data-table-row-actions.tsx

@@ -0,0 +1,67 @@
+import { DotsHorizontalIcon } from '@radix-ui/react-icons'
+import { Row } from '@tanstack/react-table'
+
+import { Button } from '@/components/ui/button'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubContent,
+  DropdownMenuSubTrigger,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+
+import { labels } from '../data/data'
+import { taskSchema } from '../data/schema'
+
+interface DataTableRowActionsProps<TData> {
+  row: Row<TData>
+}
+
+export function DataTableRowActions<TData>({
+  row,
+}: DataTableRowActionsProps<TData>) {
+  const task = taskSchema.parse(row.original)
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button
+          variant='ghost'
+          className='flex h-8 w-8 p-0 data-[state=open]:bg-muted'
+        >
+          <DotsHorizontalIcon className='h-4 w-4' />
+          <span className='sr-only'>Open menu</span>
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align='end' className='w-[160px]'>
+        <DropdownMenuItem>Edit</DropdownMenuItem>
+        <DropdownMenuItem>Make a copy</DropdownMenuItem>
+        <DropdownMenuItem>Favorite</DropdownMenuItem>
+        <DropdownMenuSeparator />
+        <DropdownMenuSub>
+          <DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
+          <DropdownMenuSubContent>
+            <DropdownMenuRadioGroup value={task.label}>
+              {labels.map((label) => (
+                <DropdownMenuRadioItem key={label.value} value={label.value}>
+                  {label.label}
+                </DropdownMenuRadioItem>
+              ))}
+            </DropdownMenuRadioGroup>
+          </DropdownMenuSubContent>
+        </DropdownMenuSub>
+        <DropdownMenuSeparator />
+        <DropdownMenuItem>
+          Delete
+          <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  )
+}

+ 61 - 0
src/pages/tasks/components/data-table-toolbar.tsx

@@ -0,0 +1,61 @@
+import { Cross2Icon } from '@radix-ui/react-icons'
+import { Table } from '@tanstack/react-table'
+
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { DataTableViewOptions } from '../components/data-table-view-options'
+
+import { priorities, statuses } from '../data/data'
+import { DataTableFacetedFilter } from './data-table-faceted-filter'
+
+interface DataTableToolbarProps<TData> {
+  table: Table<TData>
+}
+
+export function DataTableToolbar<TData>({
+  table,
+}: DataTableToolbarProps<TData>) {
+  const isFiltered = table.getState().columnFilters.length > 0
+
+  return (
+    <div className='flex items-center justify-between'>
+      <div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
+        <Input
+          placeholder='Filter tasks...'
+          value={(table.getColumn('title')?.getFilterValue() as string) ?? ''}
+          onChange={(event) =>
+            table.getColumn('title')?.setFilterValue(event.target.value)
+          }
+          className='h-8 w-[150px] lg:w-[250px]'
+        />
+        <div className='flex gap-x-2'>
+          {table.getColumn('status') && (
+            <DataTableFacetedFilter
+              column={table.getColumn('status')}
+              title='Status'
+              options={statuses}
+            />
+          )}
+          {table.getColumn('priority') && (
+            <DataTableFacetedFilter
+              column={table.getColumn('priority')}
+              title='Priority'
+              options={priorities}
+            />
+          )}
+        </div>
+        {isFiltered && (
+          <Button
+            variant='ghost'
+            onClick={() => table.resetColumnFilters()}
+            className='h-8 px-2 lg:px-3'
+          >
+            Reset
+            <Cross2Icon className='ml-2 h-4 w-4' />
+          </Button>
+        )}
+      </div>
+      <DataTableViewOptions table={table} />
+    </div>
+  )
+}

+ 57 - 0
src/pages/tasks/components/data-table-view-options.tsx

@@ -0,0 +1,57 @@
+import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'
+import { MixerHorizontalIcon } from '@radix-ui/react-icons'
+import { Table } from '@tanstack/react-table'
+
+import { Button } from '@/components/ui/button'
+import {
+  DropdownMenu,
+  DropdownMenuCheckboxItem,
+  DropdownMenuContent,
+  DropdownMenuLabel,
+  DropdownMenuSeparator,
+} from '@/components/ui/dropdown-menu'
+
+interface DataTableViewOptionsProps<TData> {
+  table: Table<TData>
+}
+
+export function DataTableViewOptions<TData>({
+  table,
+}: DataTableViewOptionsProps<TData>) {
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button
+          variant='outline'
+          size='sm'
+          className='ml-auto hidden h-8 lg:flex'
+        >
+          <MixerHorizontalIcon className='mr-2 h-4 w-4' />
+          View
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align='end' className='w-[150px]'>
+        <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
+        <DropdownMenuSeparator />
+        {table
+          .getAllColumns()
+          .filter(
+            (column) =>
+              typeof column.accessorFn !== 'undefined' && column.getCanHide()
+          )
+          .map((column) => {
+            return (
+              <DropdownMenuCheckboxItem
+                key={column.id}
+                className='capitalize'
+                checked={column.getIsVisible()}
+                onCheckedChange={(value) => column.toggleVisibility(!!value)}
+              >
+                {column.id}
+              </DropdownMenuCheckboxItem>
+            )
+          })}
+      </DropdownMenuContent>
+    </DropdownMenu>
+  )
+}

+ 124 - 0
src/pages/tasks/components/data-table.tsx

@@ -0,0 +1,124 @@
+import * as React from 'react'
+import {
+  ColumnDef,
+  ColumnFiltersState,
+  SortingState,
+  VisibilityState,
+  flexRender,
+  getCoreRowModel,
+  getFacetedRowModel,
+  getFacetedUniqueValues,
+  getFilteredRowModel,
+  getPaginationRowModel,
+  getSortedRowModel,
+  useReactTable,
+} from '@tanstack/react-table'
+
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table'
+
+import { DataTablePagination } from '../components/data-table-pagination'
+import { DataTableToolbar } from '../components/data-table-toolbar'
+
+interface DataTableProps<TData, TValue> {
+  columns: ColumnDef<TData, TValue>[]
+  data: TData[]
+}
+
+export function DataTable<TData, TValue>({
+  columns,
+  data,
+}: DataTableProps<TData, TValue>) {
+  const [rowSelection, setRowSelection] = React.useState({})
+  const [columnVisibility, setColumnVisibility] =
+    React.useState<VisibilityState>({})
+  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
+    []
+  )
+  const [sorting, setSorting] = React.useState<SortingState>([])
+
+  const table = useReactTable({
+    data,
+    columns,
+    state: {
+      sorting,
+      columnVisibility,
+      rowSelection,
+      columnFilters,
+    },
+    enableRowSelection: true,
+    onRowSelectionChange: setRowSelection,
+    onSortingChange: setSorting,
+    onColumnFiltersChange: setColumnFilters,
+    onColumnVisibilityChange: setColumnVisibility,
+    getCoreRowModel: getCoreRowModel(),
+    getFilteredRowModel: getFilteredRowModel(),
+    getPaginationRowModel: getPaginationRowModel(),
+    getSortedRowModel: getSortedRowModel(),
+    getFacetedRowModel: getFacetedRowModel(),
+    getFacetedUniqueValues: getFacetedUniqueValues(),
+  })
+
+  return (
+    <div className='space-y-4'>
+      <DataTableToolbar table={table} />
+      <div className='rounded-md border'>
+        <Table>
+          <TableHeader>
+            {table.getHeaderGroups().map((headerGroup) => (
+              <TableRow key={headerGroup.id}>
+                {headerGroup.headers.map((header) => {
+                  return (
+                    <TableHead key={header.id} colSpan={header.colSpan}>
+                      {header.isPlaceholder
+                        ? null
+                        : flexRender(
+                            header.column.columnDef.header,
+                            header.getContext()
+                          )}
+                    </TableHead>
+                  )
+                })}
+              </TableRow>
+            ))}
+          </TableHeader>
+          <TableBody>
+            {table.getRowModel().rows?.length ? (
+              table.getRowModel().rows.map((row) => (
+                <TableRow
+                  key={row.id}
+                  data-state={row.getIsSelected() && 'selected'}
+                >
+                  {row.getVisibleCells().map((cell) => (
+                    <TableCell key={cell.id}>
+                      {flexRender(
+                        cell.column.columnDef.cell,
+                        cell.getContext()
+                      )}
+                    </TableCell>
+                  ))}
+                </TableRow>
+              ))
+            ) : (
+              <TableRow>
+                <TableCell
+                  colSpan={columns.length}
+                  className='h-24 text-center'
+                >
+                  No results.
+                </TableCell>
+              </TableRow>
+            )}
+          </TableBody>
+        </Table>
+      </div>
+      <DataTablePagination table={table} />
+    </div>
+  )
+}

+ 71 - 0
src/pages/tasks/data/data.tsx

@@ -0,0 +1,71 @@
+import {
+  ArrowDownIcon,
+  ArrowRightIcon,
+  ArrowUpIcon,
+  CheckCircledIcon,
+  CircleIcon,
+  CrossCircledIcon,
+  QuestionMarkCircledIcon,
+  StopwatchIcon,
+} from '@radix-ui/react-icons'
+
+export const labels = [
+  {
+    value: 'bug',
+    label: 'Bug',
+  },
+  {
+    value: 'feature',
+    label: 'Feature',
+  },
+  {
+    value: 'documentation',
+    label: 'Documentation',
+  },
+]
+
+export const statuses = [
+  {
+    value: 'backlog',
+    label: 'Backlog',
+    icon: QuestionMarkCircledIcon,
+  },
+  {
+    value: 'todo',
+    label: 'Todo',
+    icon: CircleIcon,
+  },
+  {
+    value: 'in progress',
+    label: 'In Progress',
+    icon: StopwatchIcon,
+  },
+  {
+    value: 'done',
+    label: 'Done',
+    icon: CheckCircledIcon,
+  },
+  {
+    value: 'canceled',
+    label: 'Canceled',
+    icon: CrossCircledIcon,
+  },
+]
+
+export const priorities = [
+  {
+    label: 'Low',
+    value: 'low',
+    icon: ArrowDownIcon,
+  },
+  {
+    label: 'Medium',
+    value: 'medium',
+    icon: ArrowRightIcon,
+  },
+  {
+    label: 'High',
+    value: 'high',
+    icon: ArrowUpIcon,
+  },
+]

+ 13 - 0
src/pages/tasks/data/schema.ts

@@ -0,0 +1,13 @@
+import { z } from 'zod'
+
+// We're keeping a simple non-relational schema here.
+// IRL, you will have a schema for your data models.
+export const taskSchema = z.object({
+  id: z.string(),
+  title: z.string(),
+  status: z.string(),
+  label: z.string(),
+  priority: z.string(),
+})
+
+export type Task = z.infer<typeof taskSchema>

+ 782 - 0
src/pages/tasks/data/tasks.ts

@@ -0,0 +1,782 @@
+export const tasks = [
+  {
+    id: 'TASK-8782',
+    title:
+      "You can't compress the program without quantifying the open-source SSD pixel!",
+    status: 'in progress',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-7878',
+    title:
+      'Try to calculate the EXE feed, maybe it will index the multi-byte pixel!',
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-7839',
+    title: 'We need to bypass the neural TCP card!',
+    status: 'todo',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-5562',
+    title:
+      'The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!',
+    status: 'backlog',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-8686',
+    title:
+      "I'll parse the wireless SSL protocol, that should driver the API panel!",
+    status: 'canceled',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-1280',
+    title:
+      'Use the digital TLS panel, then you can transmit the haptic system!',
+    status: 'done',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-7262',
+    title:
+      'The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!',
+    status: 'done',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1138',
+    title:
+      "Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!",
+    status: 'in progress',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-7184',
+    title: 'We need to program the back-end THX pixel!',
+    status: 'todo',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-5160',
+    title:
+      "Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!",
+    status: 'in progress',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-5618',
+    title:
+      "Generating the driver won't do anything, we need to index the online SSL application!",
+    status: 'done',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-6699',
+    title:
+      "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-2858',
+    title: 'We need to override the online UDP bus!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-9864',
+    title:
+      "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
+    status: 'done',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-8404',
+    title: 'We need to generate the virtual HEX alarm!',
+    status: 'in progress',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-5365',
+    title:
+      "Backing up the pixel won't do anything, we need to transmit the primary IB array!",
+    status: 'in progress',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-1780',
+    title:
+      'The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!',
+    status: 'todo',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-6938',
+    title:
+      'Use the redundant SCSI application, then you can hack the optical alarm!',
+    status: 'todo',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-9885',
+    title: 'We need to compress the auxiliary VGA driver!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-3216',
+    title:
+      "Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!",
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-9285',
+    title:
+      'The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!',
+    status: 'todo',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1024',
+    title:
+      "Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!",
+    status: 'in progress',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-7068',
+    title:
+      "You can't generate the capacitor without indexing the wireless HEX pixel!",
+    status: 'canceled',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-6502',
+    title:
+      "Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!",
+    status: 'todo',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-5326',
+    title: 'We need to hack the redundant UTF8 transmitter!',
+    status: 'todo',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-6274',
+    title:
+      'Use the virtual PCI circuit, then you can parse the bluetooth alarm!',
+    status: 'canceled',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-1571',
+    title:
+      "I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
+    status: 'in progress',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-9518',
+    title:
+      "Compressing the interface won't do anything, we need to compress the online SDD matrix!",
+    status: 'canceled',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-5581',
+    title:
+      "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-2197',
+    title:
+      "Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!",
+    status: 'todo',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-8484',
+    title: 'We need to parse the solid state UDP firewall!',
+    status: 'in progress',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-9892',
+    title:
+      'If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!',
+    status: 'done',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-9616',
+    title: 'We need to synthesize the cross-platform ASCII pixel!',
+    status: 'in progress',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-9744',
+    title:
+      'Use the back-end IP card, then you can input the solid state hard drive!',
+    status: 'done',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-1376',
+    title:
+      "Generating the alarm won't do anything, we need to generate the mobile IP capacitor!",
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-7382',
+    title:
+      'If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!',
+    status: 'todo',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-2290',
+    title:
+      "I'll compress the virtual JSON panel, that should application the UTF8 bus!",
+    status: 'canceled',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1533',
+    title:
+      "You can't input the firewall without overriding the wireless TCP firewall!",
+    status: 'done',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-4920',
+    title:
+      "Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!",
+    status: 'in progress',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-5168',
+    title:
+      'If we synthesize the bus, we can get to the IP panel through the virtual TLS array!',
+    status: 'in progress',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-7103',
+    title: 'We need to parse the multi-byte EXE bandwidth!',
+    status: 'canceled',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-4314',
+    title:
+      'If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!',
+    status: 'in progress',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-3415',
+    title:
+      'Use the cross-platform XML application, then you can quantify the solid state feed!',
+    status: 'todo',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-8339',
+    title:
+      'Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!',
+    status: 'in progress',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-6995',
+    title:
+      'Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!',
+    status: 'todo',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-8053',
+    title:
+      'If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!',
+    status: 'todo',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-4336',
+    title:
+      'If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!',
+    status: 'todo',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-8790',
+    title:
+      "I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
+    status: 'done',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-8980',
+    title:
+      'Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!',
+    status: 'canceled',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-7342',
+    title: 'Use the neural CLI card, then you can parse the online port!',
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-5608',
+    title:
+      "I'll hack the haptic SSL program, that should bus the UDP transmitter!",
+    status: 'canceled',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-1606',
+    title:
+      "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
+    status: 'done',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-7872',
+    title:
+      "Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!",
+    status: 'canceled',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-4167',
+    title:
+      'Use the cross-platform SMS circuit, then you can synthesize the optical feed!',
+    status: 'canceled',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-9581',
+    title:
+      "You can't index the port without hacking the cross-platform XSS monitor!",
+    status: 'backlog',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-8806',
+    title: 'We need to bypass the back-end SSL panel!',
+    status: 'done',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-6542',
+    title:
+      'Try to quantify the RSS firewall, maybe it will quantify the open-source system!',
+    status: 'done',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-6806',
+    title:
+      'The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!',
+    status: 'canceled',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-9549',
+    title: "You can't bypass the bus without connecting the neural JBOD bus!",
+    status: 'todo',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1075',
+    title:
+      "Backing up the driver won't do anything, we need to parse the redundant RAM pixel!",
+    status: 'done',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-1427',
+    title:
+      'Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!',
+    status: 'done',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1907',
+    title:
+      "Hacking the circuit won't do anything, we need to back up the online DRAM system!",
+    status: 'todo',
+    label: 'documentation',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-4309',
+    title:
+      'If we generate the system, we can get to the TCP sensor through the optical GB pixel!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3973',
+    title:
+      "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
+    status: 'todo',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-7962',
+    title:
+      'Use the wireless RAM program, then you can hack the cross-platform feed!',
+    status: 'canceled',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-3360',
+    title:
+      "You can't quantify the program without synthesizing the neural OCR interface!",
+    status: 'done',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-9887',
+    title:
+      'Use the auxiliary ASCII sensor, then you can connect the solid state port!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3649',
+    title:
+      "I'll input the virtual USB system, that should circuit the DNS monitor!",
+    status: 'in progress',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3586',
+    title:
+      'If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!',
+    status: 'in progress',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-5150',
+    title:
+      "I'll hack the wireless XSS port, that should transmitter the IP interface!",
+    status: 'canceled',
+    label: 'feature',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3652',
+    title:
+      'The SQL interface is down, override the optical bus so we can program the ASCII interface!',
+    status: 'backlog',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-6884',
+    title:
+      'Use the digital PCI circuit, then you can synthesize the multi-byte microchip!',
+    status: 'canceled',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1591',
+    title: 'We need to connect the mobile XSS driver!',
+    status: 'in progress',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-3802',
+    title:
+      'Try to override the ASCII protocol, maybe it will parse the virtual matrix!',
+    status: 'in progress',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-7253',
+    title:
+      "Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!",
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-9739',
+    title: 'We need to hack the multi-byte HDD bus!',
+    status: 'done',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-4424',
+    title:
+      'Try to hack the HEX alarm, maybe it will connect the optical pixel!',
+    status: 'in progress',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3922',
+    title:
+      "You can't back up the capacitor without generating the wireless PCI program!",
+    status: 'backlog',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-4921',
+    title:
+      "I'll index the open-source IP feed, that should system the GB application!",
+    status: 'canceled',
+    label: 'bug',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-5814',
+    title: 'We need to calculate the 1080p AGP feed!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-2645',
+    title:
+      "Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!",
+    status: 'todo',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-4535',
+    title:
+      'Try to copy the JSON circuit, maybe it will connect the wireless feed!',
+    status: 'in progress',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-4463',
+    title: 'We need to copy the solid state AGP monitor!',
+    status: 'done',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-9745',
+    title:
+      'If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!',
+    status: 'canceled',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-2080',
+    title:
+      'If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!',
+    status: 'todo',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3838',
+    title:
+      "I'll bypass the online TCP application, that should panel the AGP system!",
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-1340',
+    title: 'We need to navigate the virtual PNG circuit!',
+    status: 'todo',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-6665',
+    title:
+      'If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!',
+    status: 'canceled',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-7585',
+    title:
+      'If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!',
+    status: 'backlog',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-6319',
+    title: 'We need to copy the multi-byte SCSI program!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-4369',
+    title: 'Try to input the SCSI bus, maybe it will generate the 1080p pixel!',
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-9035',
+    title: 'We need to override the solid state PNG array!',
+    status: 'canceled',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-3970',
+    title:
+      "You can't index the transmitter without quantifying the haptic ASCII card!",
+    status: 'todo',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-4473',
+    title:
+      "You can't bypass the protocol without overriding the neural RSS program!",
+    status: 'todo',
+    label: 'documentation',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-4136',
+    title:
+      "You can't hack the hard drive without hacking the primary JSON program!",
+    status: 'canceled',
+    label: 'bug',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-3939',
+    title:
+      'Use the back-end SQL firewall, then you can connect the neural hard drive!',
+    status: 'done',
+    label: 'feature',
+    priority: 'low',
+  },
+  {
+    id: 'TASK-2007',
+    title:
+      "I'll input the back-end USB protocol, that should bandwidth the PCI system!",
+    status: 'backlog',
+    label: 'bug',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-7516',
+    title:
+      'Use the primary SQL program, then you can generate the auxiliary transmitter!',
+    status: 'done',
+    label: 'documentation',
+    priority: 'medium',
+  },
+  {
+    id: 'TASK-6906',
+    title:
+      'Try to back up the DRAM system, maybe it will reboot the online transmitter!',
+    status: 'done',
+    label: 'feature',
+    priority: 'high',
+  },
+  {
+    id: 'TASK-5207',
+    title:
+      'The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!',
+    status: 'in progress',
+    label: 'bug',
+    priority: 'low',
+  },
+]

+ 35 - 0
src/pages/tasks/index.tsx

@@ -0,0 +1,35 @@
+import { Search } from '@/components/search'
+import ThemeSwitch from '@/components/theme-switch'
+import { UserNav } from '@/components/user-nav'
+import { DataTable } from './components/data-table'
+import { columns } from './components/columns'
+import { tasks } from './data/tasks'
+
+export default function Tasks() {
+  return (
+    <div className='flex flex-col md:h-svh'>
+      {/* ===== Top Heading ===== */}
+      <div className='flex h-16 flex-none items-center gap-4 bg-background p-4 md:px-8'>
+        <Search />
+        <div className='ml-auto flex items-center space-x-4'>
+          <ThemeSwitch />
+          <UserNav />
+        </div>
+      </div>
+
+      <div className='flex h-[calc(100%-4rem)] flex-1 flex-col overflow-hidden px-4 pb-6 pt-6 md:px-10'>
+        <div className='mb-2 flex items-center justify-between space-y-2'>
+          <div>
+            <h2 className='text-2xl font-bold tracking-tight'>Welcome back!</h2>
+            <p className='text-muted-foreground'>
+              Here&apos;s a list of your tasks for this month!
+            </p>
+          </div>
+        </div>
+        <div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0'>
+          <DataTable data={tasks} columns={columns} />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 7 - 1
src/router.tsx

@@ -34,7 +34,7 @@ const router = createBrowserRouter([
   {
     path: '/',
     lazy: async () => {
-      let AppShell = await import('./components/app-shell')
+      const AppShell = await import('./components/app-shell')
       return { Component: AppShell.default }
     },
     errorElement: <GeneralError />,
@@ -45,6 +45,12 @@ const router = createBrowserRouter([
           Component: (await import('./pages/dashboard')).default,
         }),
       },
+      {
+        path: 'tasks',
+        lazy: async () => ({
+          Component: (await import('./pages/tasks')).default,
+        }),
+      },
       {
         path: 'settings',
         lazy: async () => ({