├── reset.d.ts ├── local.db ├── .prettierignore ├── src ├── app │ ├── icon.png │ ├── opengraph-image.png │ ├── robots.ts │ ├── sitemap.ts │ ├── _lib │ │ ├── validations.ts │ │ ├── utils.ts │ │ ├── queries.ts │ │ └── actions.ts │ ├── _components │ │ ├── tasks-table-toolbar-actions.tsx │ │ ├── tasks-table-provider.tsx │ │ ├── tasks-table.tsx │ │ ├── create-task-dialog.tsx │ │ ├── delete-tasks-dialog.tsx │ │ └── create-task-form.tsx │ ├── page.tsx │ └── layout.tsx ├── lib │ ├── constants.ts │ ├── fonts.ts │ ├── handle-error.ts │ ├── data-table.ts │ ├── utils.ts │ ├── id.ts │ ├── export.ts │ └── filter-column.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── toaster.tsx │ │ ├── checkbox.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── toggle.tsx │ │ ├── toggle-group.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── table.tsx │ │ ├── drawer.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── sheet.tsx │ │ ├── command.tsx │ │ └── select.tsx │ ├── providers.tsx │ ├── tailwind-indicator.tsx │ ├── shell.tsx │ ├── kbd.tsx │ ├── layouts │ │ ├── mode-toggle.tsx │ │ └── site-header.tsx │ ├── data-table │ │ ├── data-table-view-options.tsx │ │ ├── data-table-toolbar.tsx │ │ ├── data-table-column-header.tsx │ │ ├── data-table.tsx │ │ ├── data-table-pagination.tsx │ │ ├── advanced │ │ │ ├── data-table-filter-combobox.tsx │ │ │ ├── data-table-advanced-faceted-filter.tsx │ │ │ └── data-table-advanced-toolbar.tsx │ │ ├── data-table-skeleton.tsx │ │ └── data-table-faceted-filter.tsx │ └── date-range-picker.tsx ├── db │ ├── index.ts │ ├── seed.ts │ ├── migrate.ts │ ├── utils.ts │ └── schema.ts ├── hooks │ ├── use-debounce.ts │ ├── useBodyPointerEvents.ts │ └── use-media-query.ts ├── config │ ├── site.ts │ └── data-table.ts ├── env.ts ├── types │ └── index.ts └── styles │ └── globals.css ├── public ├── images │ └── screenshot.png └── site.webmanifest ├── renovate.json ├── postcss.config.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ └── code-check.yml ├── components.json ├── drizzle ├── meta │ ├── _journal.json │ ├── 0001_snapshot.json │ └── 0000_snapshot.json ├── 0001_many_unus.sql └── 0000_normal_lorna_dane.sql ├── .vscode └── settings.json ├── drizzle.config.ts ├── next.config.js ├── .env.example ├── .gitignore ├── LICENSE.md ├── tsconfig.json ├── prettier.config.js ├── .eslintrc.cjs ├── tailwind.config.ts ├── CONTRIBUTING.md ├── package.json └── README.md /reset.d.ts: -------------------------------------------------------------------------------- 1 | import "@total-typescript/ts-reset" 2 | -------------------------------------------------------------------------------- /local.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisseth/shadcn-table/HEAD/local.db -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisseth/shadcn-table/HEAD/src/app/icon.png -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisseth/shadcn-table/HEAD/src/app/opengraph-image.png -------------------------------------------------------------------------------- /public/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisseth/shadcn-table/HEAD/public/images/screenshot.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "automerge": true 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const unknownError = "An unknown error occurred. Please try again later." 2 | 3 | export const databasePrefix = "shadcn" 4 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { GeistMono } from "geist/font/mono" 2 | import { GeistSans } from "geist/font/sans" 3 | 4 | export const fontSans = GeistSans 5 | export const fontMono = GeistMono 6 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shadcn Table", 3 | "short_name": "Shadcn Table", 4 | "icons": [ 5 | { 6 | "src": "/icon.png", 7 | "sizes": "32x32", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataRoute } from "next" 2 | 3 | import { siteConfig } from "@/config/site" 4 | 5 | export default function robots(): MetadataRoute.Robots { 6 | return { 7 | rules: { 8 | userAgent: "*", 9 | allow: "/", 10 | }, 11 | sitemap: `${siteConfig.url}/sitemap.xml`, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataRoute } from "next" 2 | 3 | import { siteConfig } from "@/config/site" 4 | 5 | export default function sitemap(): MetadataRoute.Sitemap { 6 | const routes = [""].map((route) => ({ 7 | url: `${siteConfig.url}${route}`, 8 | lastModified: new Date().toISOString(), 9 | })) 10 | 11 | return [...routes] 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This template is heavily inspired by the shadcn-ui/ui repository. 2 | # See: https://github.com/shadcn-ui/ui/blob/main/.github/ISSUE_TEMPLATE/config.yml 3 | 4 | blank_issues_enabled: false 5 | contact_links: 6 | - name: General questions 7 | url: https://github.com/sadmann7/shadcn-table/discussions?category=general 8 | about: Please ask and answer questions here 9 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "/src/styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env" 2 | import { createClient } from "@libsql/client" 3 | import { drizzle } from "drizzle-orm/libsql" 4 | 5 | import * as schema from "./schema" 6 | 7 | // const client = postgres(env.DATABASE_URL) 8 | export const client = createClient({ 9 | url: env.DATABASE_URL, 10 | authToken: env.DB_AUTH_TOKEN!, 11 | }) 12 | 13 | export const db = drizzle(client, { schema }) 14 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "mysql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1715957734792, 9 | "tag": "0000_normal_lorna_dane", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1722007572684, 16 | "tag": "0001_many_unus", 17 | "breakpoints": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /src/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = React.useState(value) 5 | 6 | React.useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500) 8 | 9 | return () => { 10 | clearTimeout(timer) 11 | } 12 | }, [value, delay]) 13 | 14 | return debouncedValue 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useBodyPointerEvents.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | 3 | const useBodyPointerEvents = (isOpen: boolean) => { 4 | useEffect(() => { 5 | if (isOpen) { 6 | document.body.style.pointerEvents = "auto" 7 | } else { 8 | document.body.style.pointerEvents = "" 9 | } 10 | 11 | return () => { 12 | document.body.style.pointerEvents = "" 13 | } 14 | }, [isOpen]) 15 | } 16 | 17 | export default useBodyPointerEvents 18 | -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env" 2 | 3 | export type SiteConfig = typeof siteConfig 4 | 5 | export const siteConfig = { 6 | name: "Table", 7 | description: 8 | "Shadcn table component with server side sorting, pagination, and filtering", 9 | url: 10 | env.NODE_ENV === "development" 11 | ? "http://localhost:3000" 12 | : "https://shadcn-table-vert.vercel.app", 13 | links: { github: "https://github.com/whoisseth/shadcn-table" }, 14 | } 15 | -------------------------------------------------------------------------------- /src/db/seed.ts: -------------------------------------------------------------------------------- 1 | import { seedTasks } from "@/app/_lib/actions" 2 | 3 | async function runSeed() { 4 | console.log("⏳ Running seed...") 5 | 6 | const start = Date.now() 7 | 8 | await seedTasks({ count: 15 }) 9 | 10 | const end = Date.now() 11 | 12 | console.log(`✅ Seed completed in ${end - start}ms`) 13 | 14 | process.exit(0) 15 | } 16 | 17 | runSeed().catch((err) => { 18 | console.error("❌ Seed failed") 19 | console.error(err) 20 | process.exit(1) 21 | }) 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.includeLanguages": { 3 | "plaintext": "html" 4 | }, 5 | "tailwindCSS.experimental.classRegex": [ 6 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 7 | ["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 8 | ["tw=\"([^\"]*)\""] 9 | ], 10 | "json.schemas": [ 11 | { 12 | "fileMatch": ["/package.json"], 13 | "url": "https://json.schemastore.org/package", 14 | "schema": true 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes" 4 | import type { ThemeProviderProps } from "next-themes/dist/types" 5 | 6 | import { TooltipProvider } from "@/components/ui/tooltip" 7 | 8 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env" 2 | import { type Config } from "drizzle-kit" 3 | 4 | import { databasePrefix } from "@/lib/constants" 5 | 6 | export default { 7 | schema: "./src/db/schema.ts", 8 | // dialect: "postgresql", 9 | dialect: "sqlite", 10 | out: "./drizzle", 11 | driver: "turso", 12 | dbCredentials: { 13 | url: env.DATABASE_URL, 14 | authToken: process.env.DB_AUTH_TOKEN!, 15 | }, 16 | tablesFilter: [`${databasePrefix}_*`], 17 | } satisfies Config 18 | -------------------------------------------------------------------------------- /src/hooks/use-media-query.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches) 9 | } 10 | 11 | const result = matchMedia(query) 12 | result.addEventListener("change", onChange) 13 | setValue(result.matches) 14 | 15 | return () => result.removeEventListener("change", onChange) 16 | }, [query]) 17 | 18 | return value 19 | } 20 | -------------------------------------------------------------------------------- /src/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/libsql/migrator" 2 | 3 | import { db } from "." 4 | 5 | export async function runMigrate() { 6 | console.log("⏳ Running migrations...") 7 | 8 | const start = Date.now() 9 | 10 | await migrate(db, { migrationsFolder: "drizzle" }) 11 | 12 | const end = Date.now() 13 | 14 | console.log(`✅ Migrations completed in ${end - start}ms`) 15 | 16 | process.exit(0) 17 | } 18 | 19 | runMigrate().catch((err) => { 20 | console.error("❌ Migration failed") 21 | console.error(err) 22 | process.exit(1) 23 | }) 24 | -------------------------------------------------------------------------------- /drizzle/0001_many_unus.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "shadcn_tasks" ALTER COLUMN "code" SET DATA TYPE varchar(128);--> statement-breakpoint 2 | ALTER TABLE "shadcn_tasks" ALTER COLUMN "code" SET NOT NULL;--> statement-breakpoint 3 | ALTER TABLE "shadcn_tasks" ALTER COLUMN "title" SET DATA TYPE varchar(128);--> statement-breakpoint 4 | ALTER TABLE "shadcn_tasks" ALTER COLUMN "status" SET DATA TYPE varchar(30);--> statement-breakpoint 5 | ALTER TABLE "shadcn_tasks" ALTER COLUMN "label" SET DATA TYPE varchar(30);--> statement-breakpoint 6 | ALTER TABLE "shadcn_tasks" ALTER COLUMN "priority" SET DATA TYPE varchar(30); -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | // await import("./src/env") 6 | 7 | // /** @type {import("next").NextConfig} */ 8 | const nextConfig = { 9 | experimental: { 10 | ppr: true, 11 | reactCompiler: { 12 | compilationMode: "annotation", 13 | }, 14 | }, 15 | // Already doing linting and typechecking as separate tasks in CI 16 | eslint: { ignoreDuringBuilds: true }, 17 | typescript: { ignoreBuildErrors: true }, 18 | } 19 | 20 | export default nextConfig 21 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs" 2 | import { z } from "zod" 3 | 4 | export const env = createEnv({ 5 | server: { 6 | NODE_ENV: z 7 | .enum(["development", "test", "production"]) 8 | .default("development"), 9 | DATABASE_URL: z.string().url(), 10 | DB_AUTH_TOKEN: z.string().optional(), 11 | }, 12 | 13 | client: {}, 14 | 15 | runtimeEnv: { 16 | DATABASE_URL: process.env.DATABASE_URL, 17 | NODE_ENV: process.env.NODE_ENV, 18 | DB_AUTH_TOKEN: process.env.DB_AUTH_TOKEN, 19 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 20 | }, 21 | skipValidation: !!process.env.SKIP_ENV_VALIDATION, 22 | emptyStringAsUndefined: true, 23 | }) 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Database 13 | # DATABASE_URL="postgres://YOUR_SQLite_URL" 14 | DATABASE_URL=file:local.db 15 | # If using Online DB like Turso 16 | DB_AUTH_TOKEN=DB_AUTH_TOKEN -------------------------------------------------------------------------------- /src/lib/handle-error.ts: -------------------------------------------------------------------------------- 1 | import { isRedirectError } from "next/dist/client/components/redirect" 2 | import { toast } from "sonner" 3 | import { z } from "zod" 4 | 5 | export function getErrorMessage(err: unknown) { 6 | const unknownError = "Something went wrong, please try again later." 7 | 8 | if (err instanceof z.ZodError) { 9 | const errors = err.issues.map((issue) => { 10 | return issue.message 11 | }) 12 | return errors.join("\n") 13 | } else if (err instanceof Error) { 14 | return err.message 15 | } else if (isRedirectError(err)) { 16 | throw err 17 | } else { 18 | return unknownError 19 | } 20 | } 21 | 22 | export function showErrorToast(err: unknown) { 23 | const errorMessage = getErrorMessage(err) 24 | return toast.error(errorMessage) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { env } from "@/env" 2 | 3 | export function TailwindIndicator() { 4 | if (env.NODE_ENV === "production") return null 5 | 6 | return ( 7 |
8 |
xs
9 |
10 | sm 11 |
12 |
md
13 |
lg
14 |
xl
15 |
2xl
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | 44 | 45 | 46 | local.db 47 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { type SQL } from "drizzle-orm" 2 | 3 | export interface SearchParams { 4 | [key: string]: string | string[] | undefined 5 | } 6 | 7 | export interface Option { 8 | label: string 9 | value: string 10 | icon?: React.ComponentType<{ className?: string }> 11 | withCount?: boolean 12 | } 13 | 14 | export interface DataTableFilterField { 15 | label: string 16 | value: keyof TData 17 | placeholder?: string 18 | options?: Option[] 19 | } 20 | 21 | export interface DataTableFilterOption { 22 | id: string 23 | label: string 24 | value: keyof TData 25 | options: Option[] 26 | filterValues?: string[] 27 | filterOperator?: string 28 | isMulti?: boolean 29 | } 30 | 31 | export type DrizzleWhere = 32 | | SQL 33 | | ((aliases: T) => SQL | undefined) 34 | | undefined 35 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |