├── .gitignore ├── README.md ├── components.json ├── eslint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── csv-flow-favicon.svg ├── hero-img-dark.png ├── hero-img.png ├── r │ └── csv-flow.json ├── sample-demo-data.csv └── vite.svg ├── registry.json ├── src ├── assets │ ├── CSV-flow-logo-white.svg │ ├── CSV-flow-logo.svg │ └── react.svg ├── components │ ├── code-block.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── theme-switch.tsx │ └── ui │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ └── tooltip.tsx ├── context │ └── theme-context.tsx ├── features │ └── csv-flow │ │ ├── components │ │ ├── file-uploader.tsx │ │ ├── map-step.tsx │ │ ├── review-step │ │ │ ├── data-table-header.tsx │ │ │ ├── editable-cell.tsx │ │ │ ├── index.tsx │ │ │ ├── table-body-row.tsx │ │ │ └── table-body.tsx │ │ ├── step-indicator.tsx │ │ └── upload-step.tsx │ │ ├── hooks │ │ ├── use-callback-ref.tsx │ │ └── use-controllable-state.tsx │ │ ├── index.tsx │ │ ├── types │ │ └── index.ts │ │ └── utils │ │ ├── csv-parse.ts │ │ ├── helpers.ts │ │ └── map-utils.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── pages │ ├── demo │ │ └── index.tsx │ ├── docs │ │ └── index.tsx │ └── home │ │ └── index.tsx ├── routeTree.gen.ts ├── routes │ ├── __root.tsx │ ├── demo.tsx │ ├── docs.tsx │ └── index.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSV Flow 2 | 3 | **CSV Flow** is an open-source React component for importing CSV files into your applications. It allows you to map CSV columns to custom fields, validate data with advanced rules, and process your CSV data seamlessly. CSV Flow is designed to work with modern React applications and uses popular libraries like shadcn/ui, React Table, and TanStack Virtualizer. 4 | 5 | --- 6 | 7 | ## Features 8 | 9 | - **Custom Field Mapping** 10 | Easily map CSV columns to your internal field names. Enable or disable custom fields and choose from multiple output options. 11 | 12 | - **Advanced Validation & Error Handling** 13 | Built-in support for: 14 | 15 | - **Type Validation & Coercion:** Automatically convert CSV strings into numbers, booleans, dates, or emails. 16 | - **Regex & Custom Validations:** Apply regex rules or custom validation functions to ensure data quality. 17 | - **Unique Validations:** Prevent duplicate entries with unique field checks. 18 | - **Custom Validation:** Implements custom validation logic via a function. 19 | - **Error Prioritization:** Critical errors (level `"error"`) override warnings and info messages. 20 | 21 | - **Seamless React Integration** 22 | Designed as a React component with customizable props for effortless integration into your projects. 23 | 24 | --- 25 | 26 | ## Installation 27 | 28 | Install CSV Flow using the shadcn CLI integration: 29 | 30 | ```bash 31 | pnpm dlx shadcn@latest add https://csv-flow.vercel.app/r/csv-flow.json 32 | ``` 33 | 34 | --- 35 | 36 | ## Demo 37 | 38 | https://youtu.be/f98MbYU-UiY?si=WI3ZQXlz-euifrFF 39 | 40 | ## Usage 41 | 42 | Below is an example of how to integrate CSV Flow into your React application: 43 | 44 | ```tsx 45 | import React, { useState } from "react"; 46 | import CsvFlow from "@/components/csv-flow"; 47 | import { csvFlowFieldsConfig } from "@/configs/csvFlowFieldsConfig"; 48 | 49 | function App() { 50 | const [open, setOpen] = useState(true); 51 | const handleImport = (data: Record[]) => { 52 | // Process the imported data (e.g. send to your backend API) 53 | console.log("Imported Data:", data); 54 | }; 55 | 56 | return ( 57 |
58 | 59 | 69 |
70 | ); 71 | } 72 | 73 | export default App; 74 | ``` 75 | 76 | --- 77 | 78 | ## Documentation 79 | 80 | For detailed documentation, please visit our [Documentation Page](https://csv-flow.vercel.app/docs). 81 | 82 | --- 83 | 84 | ## Demo & Playground 85 | 86 | To help you get started, we provide a demo page where you can: 87 | 88 | - **Open the CSV Flow importer:** Upload and process a CSV file using a sample configuration. 89 | - **Download sample CSV data:** Quickly download a sample file from the public folder. 90 | - **View Imported Data:** See the cleaned and validated data after import. 91 | 92 | Check out our [Demo Page](https://csv-flow.vercel.app/demo) to see CSV Flow in action. 93 | 94 | --- 95 | 96 | ## Contributing 97 | 98 | Contributions are welcome! If you find a bug or have suggestions for improvements, please open an issue or submit a pull request. 99 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CSV Flow: A Powerful React CSV Importer for Modern Apps 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-flow", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "registry:build": "shadcn build" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^3.10.0", 15 | "@radix-ui/react-checkbox": "^1.1.4", 16 | "@radix-ui/react-dialog": "^1.1.4", 17 | "@radix-ui/react-dropdown-menu": "^2.1.4", 18 | "@radix-ui/react-label": "^2.1.1", 19 | "@radix-ui/react-progress": "^1.1.1", 20 | "@radix-ui/react-scroll-area": "^1.2.2", 21 | "@radix-ui/react-select": "^2.1.5", 22 | "@radix-ui/react-separator": "^1.1.1", 23 | "@radix-ui/react-slot": "^1.1.1", 24 | "@radix-ui/react-switch": "^1.1.3", 25 | "@radix-ui/react-tooltip": "^1.1.8", 26 | "@tanstack/react-router": "^1.114.17", 27 | "@tanstack/react-table": "^8.21.2", 28 | "@tanstack/react-virtual": "^3.13.2", 29 | "@types/papaparse": "^5.3.15", 30 | "@uiw/react-json-view": "2.0.0-alpha.30", 31 | "class-variance-authority": "^0.7.1", 32 | "clsx": "^2.1.1", 33 | "date-fns": "^4.1.0", 34 | "lucide-react": "^0.473.0", 35 | "nanoid": "^5.0.9", 36 | "papaparse": "^5.5.1", 37 | "react": "^18.3.1", 38 | "react-dom": "^18.3.1", 39 | "react-dropzone": "^14.3.5", 40 | "react-hook-form": "^7.54.2", 41 | "shadcn": "2.4.0-canary.13", 42 | "sonner": "^1.7.2", 43 | "tailwind-merge": "^2.6.0", 44 | "tailwind-scrollbar": "3.1.0", 45 | "tailwindcss-animate": "^1.0.7", 46 | "zod": "^3.24.1" 47 | }, 48 | "devDependencies": { 49 | "@eslint/js": "^9.17.0", 50 | "@tanstack/react-router-devtools": "^1.114.21", 51 | "@tanstack/router-plugin": "^1.114.17", 52 | "@types/node": "^22.10.7", 53 | "@types/react": "^18.3.18", 54 | "@types/react-dom": "^18.3.5", 55 | "@vitejs/plugin-react-swc": "^3.5.0", 56 | "autoprefixer": "^10.4.20", 57 | "eslint": "^9.17.0", 58 | "eslint-plugin-react-hooks": "^5.0.0", 59 | "eslint-plugin-react-refresh": "^0.4.16", 60 | "globals": "^15.14.0", 61 | "postcss": "^8.5.1", 62 | "tailwindcss": "^3.4.17", 63 | "typescript": "~5.6.2", 64 | "typescript-eslint": "^8.18.2", 65 | "vite": "^6.0.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/csv-flow-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/hero-img-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khuranamanan/csv-flow/5c516023ebd61b6eb3b9c96da28fb9cd3d9f73d3/public/hero-img-dark.png -------------------------------------------------------------------------------- /public/hero-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khuranamanan/csv-flow/5c516023ebd61b6eb3b9c96da28fb9cd3d9f73d3/public/hero-img.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry.json", 3 | "name": "csv-flow", 4 | "homepage": "https://csv-flow.vercel.app/", 5 | "items": [ 6 | { 7 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 8 | "name": "csv-flow", 9 | "type": "registry:block", 10 | "title": "CSV Flow", 11 | "description": "A Component that makes importing CSV files into applications easier with validations.", 12 | "author": "Manan Khurana ", 13 | "registryDependencies": [ 14 | "button", 15 | "checkbox", 16 | "dialog", 17 | "form", 18 | "label", 19 | "progress", 20 | "scroll-area", 21 | "select", 22 | "separator", 23 | "sonner", 24 | "switch", 25 | "table", 26 | "tooltip" 27 | ], 28 | "dependencies": [ 29 | "date-fns", 30 | "nanoid", 31 | "papaparse", 32 | "@types/papaparse", 33 | "react-dropzone", 34 | "@tanstack/react-table", 35 | "@tanstack/react-virtual" 36 | ], 37 | "files": [ 38 | { 39 | "path": "src/features/csv-flow/index.tsx", 40 | "type": "registry:block", 41 | "target": "components/csv-flow/index.tsx" 42 | }, 43 | { 44 | "path": "src/features/csv-flow/types/index.ts", 45 | "type": "registry:block", 46 | "target": "components/csv-flow/types/index.ts" 47 | }, 48 | { 49 | "path": "src/features/csv-flow/components/file-uploader.tsx", 50 | "type": "registry:block", 51 | "target": "components/csv-flow/components/file-uploader.tsx" 52 | }, 53 | { 54 | "path": "src/features/csv-flow/hooks/use-callback-ref.tsx", 55 | "type": "registry:block", 56 | "target": "components/csv-flow/hooks/use-callback-ref.tsx" 57 | }, 58 | { 59 | "path": "src/features/csv-flow/hooks/use-controllable-state.tsx", 60 | "type": "registry:block", 61 | "target": "components/csv-flow/hooks/use-controllable-state.tsx" 62 | }, 63 | { 64 | "path": "src/features/csv-flow/components/upload-step.tsx", 65 | "type": "registry:block", 66 | "target": "components/csv-flow/components/upload-step.tsx" 67 | }, 68 | { 69 | "path": "src/features/csv-flow/components/step-indicator.tsx", 70 | "type": "registry:block", 71 | "target": "components/csv-flow/components/step-indicator.tsx" 72 | }, 73 | { 74 | "path": "src/features/csv-flow/components/map-step.tsx", 75 | "type": "registry:block", 76 | "target": "components/csv-flow/components/map-step.tsx" 77 | }, 78 | { 79 | "path": "src/features/csv-flow/utils/csv-parse.ts", 80 | "type": "registry:block", 81 | "target": "components/csv-flow/utils/csv-parse.ts" 82 | }, 83 | { 84 | "path": "src/features/csv-flow/utils/map-utils.ts", 85 | "type": "registry:block", 86 | "target": "components/csv-flow/utils/map-utils.ts" 87 | }, 88 | { 89 | "path": "src/features/csv-flow/utils/helpers.ts", 90 | "type": "registry:block", 91 | "target": "components/csv-flow/utils/helpers.ts" 92 | }, 93 | { 94 | "path": "src/features/csv-flow/components/review-step/index.tsx", 95 | "type": "registry:block", 96 | "target": "components/csv-flow/components/review-step/index.tsx" 97 | }, 98 | { 99 | "path": "src/features/csv-flow/components/review-step/data-table-header.tsx", 100 | "type": "registry:block", 101 | "target": "components/csv-flow/components/review-step/data-table-header.tsx" 102 | }, 103 | { 104 | "path": "src/features/csv-flow/components/review-step/editable-cell.tsx", 105 | "type": "registry:block", 106 | "target": "components/csv-flow/components/review-step/editable-cell.tsx" 107 | }, 108 | { 109 | "path": "src/features/csv-flow/components/review-step/table-body-row.tsx", 110 | "type": "registry:block", 111 | "target": "components/csv-flow/components/review-step/table-body-row.tsx" 112 | }, 113 | { 114 | "path": "src/features/csv-flow/components/review-step/table-body.tsx", 115 | "type": "registry:block", 116 | "target": "components/csv-flow/components/review-step/table-body.tsx" 117 | } 118 | ] 119 | } 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /src/assets/CSV-flow-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Check, Copy } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | import { Button } from "@/components/ui/button"; 6 | 7 | type CodeBlockProps = { 8 | language: string; 9 | code: string; 10 | className?: string; 11 | }; 12 | 13 | export function CodeBlock(props: CodeBlockProps) { 14 | const { language, code, className } = props; 15 | 16 | const [copied, setCopied] = useState(false); 17 | 18 | const onCopy = () => { 19 | navigator.clipboard.writeText(code); 20 | setCopied(true); 21 | setTimeout(() => setCopied(false), 2000); 22 | }; 23 | 24 | return ( 25 |
26 |
27 |
{language}
28 | 41 |
42 |
43 |         {code}
44 |       
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@tanstack/react-router"; 2 | 3 | export function Footer() { 4 | return ( 5 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import csvFlowLogo from "@/assets/CSV-flow-logo.svg"; 2 | import csvFlowLogoWhite from "@/assets/CSV-flow-logo-white.svg"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 5 | import { Link } from "@tanstack/react-router"; 6 | import { Github, Menu } from "lucide-react"; 7 | import { ThemeSwitch } from "./theme-switch"; 8 | 9 | export function Header() { 10 | return ( 11 |
12 |
13 |
14 | 15 | 16 | 20 | 21 | 49 |
50 |
51 | 52 | 57 | 61 | 62 | 63 | 64 | 72 | 73 | 77 | 103 | 104 | 105 |
106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /src/components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | import { useTheme } from "@/context/theme-context"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import { CheckIcon, MoonIcon, SunIcon } from "lucide-react"; 12 | 13 | export function ThemeSwitch() { 14 | const { theme, setTheme } = useTheme(); 15 | 16 | /* Update theme-color meta tag 17 | * when theme is updated */ 18 | useEffect(() => { 19 | const themeColor = theme === "dark" ? "#09090b" : "#fff"; 20 | const metaThemeColor = document.querySelector("meta[name='theme-color']"); 21 | if (metaThemeColor) metaThemeColor.setAttribute("content", themeColor); 22 | }, [theme]); 23 | 24 | return ( 25 | 26 | 27 | 32 | 33 | 34 | setTheme("light")}> 35 | Light{" "} 36 | 40 | 41 | setTheme("dark")}> 42 | Dark 43 | 47 | 48 | setTheme("system")}> 49 | System 50 | 54 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { Check } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { X } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ) 66 | DialogHeader.displayName = "DialogHeader" 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ) 80 | DialogFooter.displayName = "DialogFooter" 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )) 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogTrigger, 114 | DialogClose, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | } 121 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 3 | import { Check, ChevronRight, Circle } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const DropdownMenu = DropdownMenuPrimitive.Root 8 | 9 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 10 | 11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 12 | 13 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 14 | 15 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 16 | 17 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 18 | 19 | const DropdownMenuSubTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef & { 22 | inset?: boolean 23 | } 24 | >(({ className, inset, children, ...props }, ref) => ( 25 | 34 | {children} 35 | 36 | 37 | )) 38 | DropdownMenuSubTrigger.displayName = 39 | DropdownMenuPrimitive.SubTrigger.displayName 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )) 54 | DropdownMenuSubContent.displayName = 55 | DropdownMenuPrimitive.SubContent.displayName 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 72 | 73 | )) 74 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 75 | 76 | const DropdownMenuItem = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef & { 79 | inset?: boolean 80 | } 81 | >(({ className, inset, ...props }, ref) => ( 82 | svg]:size-4 [&>svg]:shrink-0", 86 | inset && "pl-8", 87 | className 88 | )} 89 | {...props} 90 | /> 91 | )) 92 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 93 | 94 | const DropdownMenuCheckboxItem = React.forwardRef< 95 | React.ElementRef, 96 | React.ComponentPropsWithoutRef 97 | >(({ className, children, checked, ...props }, ref) => ( 98 | 107 | 108 | 109 | 110 | 111 | 112 | {children} 113 | 114 | )) 115 | DropdownMenuCheckboxItem.displayName = 116 | DropdownMenuPrimitive.CheckboxItem.displayName 117 | 118 | const DropdownMenuRadioItem = React.forwardRef< 119 | React.ElementRef, 120 | React.ComponentPropsWithoutRef 121 | >(({ className, children, ...props }, ref) => ( 122 | 130 | 131 | 132 | 133 | 134 | 135 | {children} 136 | 137 | )) 138 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 139 | 140 | const DropdownMenuLabel = React.forwardRef< 141 | React.ElementRef, 142 | React.ComponentPropsWithoutRef & { 143 | inset?: boolean 144 | } 145 | >(({ className, inset, ...props }, ref) => ( 146 | 155 | )) 156 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 157 | 158 | const DropdownMenuSeparator = React.forwardRef< 159 | React.ElementRef, 160 | React.ComponentPropsWithoutRef 161 | >(({ className, ...props }, ref) => ( 162 | 167 | )) 168 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 169 | 170 | const DropdownMenuShortcut = ({ 171 | className, 172 | ...props 173 | }: React.HTMLAttributes) => { 174 | return ( 175 | 179 | ) 180 | } 181 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 182 | 183 | export { 184 | DropdownMenu, 185 | DropdownMenuTrigger, 186 | DropdownMenuContent, 187 | DropdownMenuItem, 188 | DropdownMenuCheckboxItem, 189 | DropdownMenuRadioItem, 190 | DropdownMenuLabel, 191 | DropdownMenuSeparator, 192 | DropdownMenuShortcut, 193 | DropdownMenuGroup, 194 | DropdownMenuPortal, 195 | DropdownMenuSub, 196 | DropdownMenuSubContent, 197 | DropdownMenuSubTrigger, 198 | DropdownMenuRadioGroup, 199 | } 200 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |