DirectoryFast
  • 🚀Get Started
  • 🔤Tutorials
    • Bring me to life!
    • How it's structured?
    • Make it yours
    • Bonus
  • 🛠️Features
    • AI
      • AI URL Scanner & Scan Jobs
    • Analytics
    • Authentication
    • API
      • Protected Endpoints
    • Blog
    • Database
    • Emails
    • Error Pages
    • Icons
    • Payments
    • Private Pages
    • SEO
  • 📦General Components
    • Shadcn/ui Components
    • Navbar
    • Footer
    • SignIn Modal
    • Hero Section
    • Social Proof
    • Collections Social Proof
    • Featured Section
    • Latest Collections Section
    • Latest Products Section
    • Recommended Section
  • 📦Directory Components
    • Product Card
    • Collection Card
    • Product Note
    • Search Bar
    • Tags
    • Product Options Toggle
    • Combobox
    • Multi-Combobox
    • Submit Product
    • Feature Product
    • Manage Note
    • Manage Collection
    • Dashboard Tables
  • ⛓️Links
    • GitHub Repository
    • Support
Powered by GitBook
On this page
  1. Directory Components

Dashboard Tables

Dashboard tables made easy with a 2 files config!

PreviousManage Collection

Last updated 11 months ago

  • Component: <TableComponent/>

  • Path: /components/table/table.tsx

  • Import:

import { TableComponent } from "@/components/table/table";

Table components can be tedious to set up...

To add a new table to your dashboard (or wherever you need one), simply follow these 3 steps:

  1. Add the Table component with your data to your page:

import { TableComponent } from "@/components/table/table";
import { [YOUR_FOLDER]_columns } from "data/dashboard/[YOUR_FOLDER]/columns";
...
<TableComponent
    data={data}
    columns={[YOUR_FOLDER]_columns}
    isLoading={isLoading}
    error={error}
    sort={{ id: "title", desc: true }}
/>
  1. Define the columns in /data/config/[YOUR_FOLDER]/columns.tsx:

Suggestion Table Columns example
"use client";

import { Suggestion } from "@prisma/client";
import { ColumnDef } from "@tanstack/react-table";
import { ExternalLink } from "lucide-react";

import { DataTableColumnHeader } from "@/components/table/table-column-header";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { statuses } from "@/lib/tables/data";

import { DataTableRowActions } from "./actions";

interface SuggestionWithCategory extends Suggestion {
  category: {
    name: string;
  };
}

export const suggestions_columns: ColumnDef<SuggestionWithCategory>[] = [
  {
    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="Suggestion" />
    ),
    cell: ({ row }) => (
      <TooltipProvider delayDuration={100}>
        <Tooltip>
          <TooltipTrigger asChild>
            <div className="w-16 truncate">{row.getValue("id")}</div>
          </TooltipTrigger>
          <TooltipContent>
            <p>{row.getValue("id")}</p>
          </TooltipContent>
        </Tooltip>
      </TooltipProvider>
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "title",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Title" />
    ),
    cell: ({ row }) => <div className="w-48">{row.getValue("title")}</div>,
    enableHiding: false,
  },
  {
    accessorKey: "owner",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Owner" />
    ),
    cell: ({ row }) => <div className="w-32">{row.getValue("owner")}</div>,
    enableHiding: false,
  },
  {
    accessorKey: "category",
    accessorFn: (row) => row.category?.name,
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Category" />
    ),
    cell: ({ row }) => (
      <div className="w-32">
        <Badge>{row.getValue("category")}</Badge>
      </div>
    ),
    enableHiding: false,
  },
  {
    accessorKey: "link",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Link" />
    ),
    cell: ({ row }) => (
      <TooltipProvider delayDuration={100}>
        <Tooltip>
          <TooltipTrigger asChild>
            <a
              href={row.getValue("link")}
              target="_blank"
              rel="noreferrer"
              className="w-[80px]"
            >
              <Button variant="outline" size="icon">
                <ExternalLink className="h-4 w-4" />
              </Button>
            </a>
          </TooltipTrigger>
          <TooltipContent>
            <p>{row.getValue("link")}</p>
          </TooltipContent>
        </Tooltip>
      </TooltipProvider>
    ),
    enableSorting: false,
  },
  {
    accessorKey: "comment",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Comment" />
    ),
    cell: ({ row }) => (
      <div className="line-clamp-3 w-64">{row.getValue("comment")}</div>
    ),
    enableSorting: false,
  },
  {
    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 (
        <Badge variant="secondary">
          {status.icon && (
            <status.icon className="mr-2 h-4 w-4 text-muted-foreground" />
          )}
          <span>{status.label}</span>
        </Badge>
      );
    },
    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} />,
  },
];
  1. Create your Actions page (delete/edit...) in the same folder /data/config/[YOUR_FOLDER]/actions.tsx

    Suggestion Table Actions example
    "use client";
    
    import { useState } from "react";
    
    import { zodResolver } from "@hookform/resolvers/zod";
    import { Category } from "@prisma/client";
    import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
    import { Row } from "@tanstack/react-table";
    import { MoreHorizontal } from "lucide-react";
    import { useSession } from "next-auth/react";
    import { useForm } from "react-hook-form";
    import * as z from "zod";
    
    import { LoadingSpinner } from "@/components/shared/icons";
    import { Button } from "@/components/ui/button";
    import { Dialog, DialogTrigger } from "@/components/ui/dialog";
    import {
      DialogContent,
      DialogDescription,
      DialogFooter,
      DialogHeader,
      DialogTitle,
    } from "@/components/ui/dialog";
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuSeparator,
      DropdownMenuTrigger,
    } from "@/components/ui/dropdown-menu";
    import {
      Form,
      FormControl,
      FormDescription,
      FormField,
      FormItem,
      FormLabel,
      FormMessage,
    } from "@/components/ui/form";
    import { Input } from "@/components/ui/input";
    import {
      Select,
      SelectContent,
      SelectItem,
      SelectTrigger,
      SelectValue,
    } from "@/components/ui/select";
    import { Textarea } from "@/components/ui/textarea";
    import { toast, useToast } from "@/components/ui/use-toast";
    import { metadata } from "@/data/config/metadata";
    
    interface DataTableRowActionsProps<TData> {
      row: Row<TData>;
    }
    
    export function DataTableRowActions<TData>({
      row,
    }: DataTableRowActionsProps<TData>) {
      const [open, setOpen] = useState(false);
    
      return (
        <Dialog open={open} onOpenChange={setOpen}>
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button
                variant="ghost"
                className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
              >
                <MoreHorizontal className="h-4 w-4" />
                <span className="sr-only">Open menu</span>
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end" className="w-[160px]">
              <DialogTrigger asChild>
                <DropdownMenuItem>Edit</DropdownMenuItem>
              </DialogTrigger>
              <DropdownMenuSeparator />
              <TableActionDelete row={row} />
            </DropdownMenuContent>
          </DropdownMenu>
          <TableActionEdit setOpen={setOpen} row={row} />
        </Dialog>
      );
    }
    
    //EDIT
    const formSchema = z.object({
      title: z.string().min(2).max(150),
      owner: z.string().min(2).max(150),
      category: z.union([z.string().min(2).max(100).optional(), z.literal("")]),
      link: z.union([z.string().min(5).max(255).optional(), z.literal("")]),
      comment: z.union([z.string().min(3).max(255).optional(), z.literal("")]),
    });
    
    export const TableActionEdit = ({
      setOpen,
      row,
    }: {
      setOpen: React.Dispatch<React.SetStateAction<boolean>>;
      row: any;
    }) => {
      const queryClient: any = useQueryClient();
    
      const session = useSession();
      const userId = session?.data?.user?.id;
    
      const {
        isLoading,
        error,
        data: categories,
      } = useQuery({
        queryKey: ["categories"],
        queryFn: async () => {
          const res = await fetch("/api/categories");
          return res.json();
        },
      });
    
      const { toast } = useToast();
    
      const form = useForm<z.infer<typeof formSchema>>({
        resolver: zodResolver(formSchema),
        defaultValues: {
          title: row.original.title || undefined,
          owner: row.original.owner || undefined,
          category: row.original.category.slug || undefined,
          link: row.original.link || undefined,
          comment: row.original.comment || undefined,
        },
      });
    
      const mutation = useMutation({
        mutationFn: async (values: z.infer<typeof formSchema>) => {
          const res = await fetch(`/api/suggestions/${row.original.id}`, {
            method: "PATCH",
            body: JSON.stringify(values),
            headers: {
              "Content-Type": "application/json",
            },
          });
          return res.json();
        },
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: [`product-sub-${userId}`] });
          toast({
            title: `Updating ${metadata.productLabel} suggestion`,
            description: `Your ${metadata.productLabel} suggestion has been updated before review.`,
          });
          setOpen(false);
          form.reset();
        },
        onError: () => {
          toast({
            title: "Error",
            description: `An error occurred while submitting your ${metadata.productLabel}.`,
            variant: "destructive",
          });
        },
      });
    
      function onSubmit(values: z.infer<typeof formSchema>) {
        mutation.mutate(values);
      }
    
      return (
        <>
          <DialogContent className="sm:max-w-[425px]">
            <DialogHeader>
              <DialogTitle>Edit a {metadata.productLabel} suggestion</DialogTitle>
              <DialogDescription>
                You can edit your {metadata.productLabel} suggestion before it is
                reviewed.
              </DialogDescription>
            </DialogHeader>
            <Form {...form}>
              <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
                <div className="grid gap-4 py-4">
                  <FormField
                    control={form.control}
                    name="title"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel className="capitalize">
                          {metadata.productLabel} Title
                        </FormLabel>
                        <FormControl>
                          <Input
                            placeholder={`An awesome ${metadata.productLabel}`}
                            defaultValue={field.value}
                            {...field}
                          />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={form.control}
                    name="owner"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Owner</FormLabel>
                        <FormControl>
                          <Input
                            placeholder="An awesome guy"
                            defaultValue={field.value}
                            {...field}
                          />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={form.control}
                    name="category"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Category</FormLabel>
                        <Select
                          onValueChange={field.onChange}
                          defaultValue={field.value}
                        >
                          <FormControl>
                            <SelectTrigger>
                              <SelectValue placeholder="Select a category" />
                            </SelectTrigger>
                          </FormControl>
                          <SelectContent>
                            {isLoading && <LoadingSpinner />}
                            {error && "An error has occurred, please try again."}
                            {categories?.map((category: Category) => (
                              <SelectItem key={category.id} value={category.slug}>
                                {category.name}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={form.control}
                    name="link"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>URL</FormLabel>
                        <FormControl>
                          <Input
                            placeholder="https://awesomeproduct.com"
                            defaultValue={field.value}
                            {...field}
                          />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={form.control}
                    name="comment"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Comment</FormLabel>
                        <FormControl>
                          <Textarea
                            maxLength={255}
                            placeholder={`This ${metadata.productLabel} is a good addition because...`}
                            defaultValue={field.value}
                            {...field}
                            className="resize-none"
                          />
                        </FormControl>
                        <FormDescription>
                          This is optional, but it will help us review your{" "}
                          {metadata.productLabel}
                          faster.
                        </FormDescription>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                </div>
                <DialogFooter>
                  <Button type="submit">Suggest {metadata.productLabel}</Button>
                </DialogFooter>
              </form>
            </Form>
          </DialogContent>
        </>
      );
    };
    
    // DELETE
    function TableActionDelete<TData>({ row }: DataTableRowActionsProps<TData>) {
      const queryClient = useQueryClient();
    
      const session = useSession();
      const userId = session?.data?.user?.id;
    
      const suggestion: any = row.original;
      const deleteMutation = useMutation({
        mutationKey: [`suggestion-${suggestion.id}`],
        mutationFn: async () => {
          const res = await fetch(`/api/suggestions/${suggestion.id}`, {
            method: "DELETE",
          });
          return res.json();
        },
        onSuccess: () => {
          toast({
            title: "Suggestion deleted",
            description: "The suggestion has been deleted.",
          });
          queryClient.invalidateQueries({ queryKey: [`product-sub-${userId}`] });
        },
        onError: () => {
          toast({
            title: "Error",
            description: "Something went wrong. Please try again.",
            variant: "destructive",
          });
        },
      });
      return (
        <DropdownMenuItem onClick={() => deleteMutation.mutate()}>
          Delete
        </DropdownMenuItem>
      );
    }
    

Your Table is now functional!

DirectoryFast attempts to solve this problem by using and simplifying the already excellent components, and adding its own logic for maximum reusability.

Check for more informations.

If you want to build a more specific table, follow the comprehensive .

📦
Shadcn/ui Table
TanStack-Table documentation
Shadcn/ui guide
Dashboard Table