├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── layout.tsx ├── globals.css └── page.tsx ├── images ├── simple.png └── overview.png ├── next.config.mjs ├── .eslintrc.json ├── postcss.config.mjs ├── lib └── utils.ts ├── vercel.json ├── .storybook ├── manager.ts ├── preview.ts ├── main.ts └── manager-head.html ├── components.json ├── .gitignore ├── tsconfig.json ├── components ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── tooltip.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── simple-time-picker.tsx ├── datetime-input.tsx └── datetime-picker.tsx ├── tailwind.config.ts ├── package.json ├── stories ├── DateTimeInput.stories.tsx ├── SimpleTimePicker.stories.tsx └── DateTimePicker.stories.tsx └── README.md /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huybuidac/shadcn-datetime-picker/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /images/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huybuidac/shadcn-datetime-picker/HEAD/images/simple.png -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huybuidac/shadcn-datetime-picker/HEAD/images/overview.png -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huybuidac/shadcn-datetime-picker/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huybuidac/shadcn-datetime-picker/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-empty-object-type": "off" 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "buildCommand": "npm run build-storybook", 4 | "devCommand": "npm run storybook", 5 | "installCommand": "npm install", 6 | "framework": null, 7 | "outputDirectory": "./storybook-static" 8 | } -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | import { create } from 'storybook/internal/theming'; 3 | 4 | addons.setConfig({ 5 | theme: create({ 6 | brandTitle: 'Shadcn Datetime Picker', 7 | base: 'light', 8 | brandUrl: 'https://github.com/huybuidac/shadcn-datetime-picker', 9 | }), 10 | }); 11 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import '../app/globals.css' 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | options: { 13 | storySort: { 14 | order: ['DateTimePicker', 'DateTimeInput', 'SimpleTimePicker'], 15 | } 16 | }, 17 | }, 18 | }; 19 | 20 | export default preview; 21 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "app/globals.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 | } -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | *storybook.log 39 | 40 | storybook-static -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/nextjs"; 2 | 3 | const config: StorybookConfig = { 4 | stories: [ 5 | "../stories/**/*.mdx", 6 | "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", 7 | ], 8 | addons: [ 9 | "@storybook/addon-onboarding", 10 | "@storybook/addon-links", 11 | "@storybook/addon-essentials", 12 | "@chromatic-com/storybook", 13 | "@storybook/addon-interactions", 14 | "@storybook/addon-styling-webpack", 15 | "@storybook/addon-links", 16 | "@storybook/addon-storysource" 17 | ], 18 | framework: { 19 | name: "@storybook/nextjs", 20 | options: {}, 21 | }, 22 | staticDirs: [{ from: "../images", to: "/assets" }], 23 | }; 24 | export default config; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Shadcn Datetime Picker", 18 | description: "Shadcn Datetime Picker", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )) 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 33 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | Shadcn Datetime Picker 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadcn-datetime-picker", 3 | "version": "0.1.0", 4 | "private": false, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/huybuidac/shadcn-datetime-picker" 8 | }, 9 | "scripts": { 10 | "dev": "next dev", 11 | "build": "next build", 12 | "start": "next start", 13 | "lint": "next lint", 14 | "storybook": "storybook dev -p 6006", 15 | "build-storybook": "storybook build" 16 | }, 17 | "dependencies": { 18 | "@hookform/resolvers": "^3.9.0", 19 | "@radix-ui/react-dialog": "^1.1.2", 20 | "@radix-ui/react-dropdown-menu": "^2.1.2", 21 | "@radix-ui/react-icons": "^1.3.0", 22 | "@radix-ui/react-label": "^2.1.0", 23 | "@radix-ui/react-popover": "^1.1.2", 24 | "@radix-ui/react-scroll-area": "^1.2.0", 25 | "@radix-ui/react-select": "^2.1.2", 26 | "@radix-ui/react-slot": "^1.1.0", 27 | "@radix-ui/react-tooltip": "^1.1.3", 28 | "class-variance-authority": "^0.7.0", 29 | "clsx": "^2.1.1", 30 | "lucide-react": "^0.447.0", 31 | "next": "14.2.14", 32 | "react": "^18", 33 | "react-day-picker": "^9.1.3", 34 | "react-dom": "^18", 35 | "react-hook-form": "^7.53.1", 36 | "tailwind-merge": "^2.5.3", 37 | "tailwindcss-animate": "^1.0.7", 38 | "zod": "^3.23.8" 39 | }, 40 | "devDependencies": { 41 | "@chromatic-com/storybook": "^1.9.0", 42 | "@storybook/addon-essentials": "^8.3.6", 43 | "@storybook/addon-interactions": "^8.3.6", 44 | "@storybook/addon-links": "^8.3.6", 45 | "@storybook/addon-onboarding": "^8.3.6", 46 | "@storybook/addon-storysource": "^8.3.6", 47 | "@storybook/addon-styling-webpack": "^1.0.0", 48 | "@storybook/blocks": "^8.3.6", 49 | "@storybook/nextjs": "^8.3.6", 50 | "@storybook/react": "^8.3.6", 51 | "@storybook/test": "^8.3.6", 52 | "@types/node": "^20", 53 | "@types/react": "^18", 54 | "@types/react-dom": "^18", 55 | "eslint": "^8", 56 | "eslint-config-next": "14.2.14", 57 | "eslint-plugin-storybook": "^0.9.0", 58 | "postcss": "^8", 59 | "storybook": "^8.3.6", 60 | "tailwindcss": "^3.4.1", 61 | "typescript": "^5" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 240 10% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 240 10% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 240 10% 3.9%; 23 | --primary: 240 5.9% 10%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | --muted: 240 4.8% 95.9%; 28 | --muted-foreground: 240 3.8% 46.1%; 29 | --accent: 240 4.8% 95.9%; 30 | --accent-foreground: 240 5.9% 10%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 240 5.9% 90%; 34 | --input: 240 5.9% 90%; 35 | --ring: 240 10% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | } 43 | .dark { 44 | --background: 240 10% 3.9%; 45 | --foreground: 0 0% 98%; 46 | --card: 240 10% 3.9%; 47 | --card-foreground: 0 0% 98%; 48 | --popover: 240 10% 3.9%; 49 | --popover-foreground: 0 0% 98%; 50 | --primary: 0 0% 98%; 51 | --primary-foreground: 240 5.9% 10%; 52 | --secondary: 240 3.7% 15.9%; 53 | --secondary-foreground: 0 0% 98%; 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | --accent: 240 3.7% 15.9%; 57 | --accent-foreground: 0 0% 98%; 58 | --destructive: 0 62.8% 30.6%; 59 | --destructive-foreground: 0 0% 98%; 60 | --border: 240 3.7% 15.9%; 61 | --input: 240 3.7% 15.9%; 62 | --ring: 240 4.9% 83.9%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /stories/DateTimeInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { DateTimeInput } from '../components/datetime-input'; 4 | 5 | import '../app/globals.css'; 6 | import { useState } from 'react'; 7 | 8 | const meta = { 9 | title: 'DateTimeInput', 10 | component: DateTimeInput, 11 | parameters: { 12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 13 | storySource: { 14 | source: 'https://github.com/huybuidac/shadcn-datetime-picker', 15 | }, 16 | }, 17 | tags: ['autodocs'], 18 | // More on argTypes: https://storybook.js.org/docs/api/argtypes 19 | argTypes: { 20 | format: { control: 'text', table: { defaultValue: { summary: 'dd/MM/yyyy-hh:mm aa' } } }, 21 | }, 22 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 23 | args: { 24 | }, 25 | decorators: [ 26 | (Story, info) => ( 27 |
28 | {info.name === 'Default' && ( 29 |
30 | Repository: 31 | 37 | shadcn-datetime-picker 38 | 39 |
40 | )} 41 | 42 |
43 | ), 44 | ], 45 | } satisfies Meta; 46 | 47 | export default meta; 48 | type Story = StoryObj; 49 | 50 | export const Default: Story = { 51 | tags: ['DEFAULT'], 52 | }; 53 | 54 | export const HideCalendarIcon: Story = { 55 | args: { 56 | hideCalendarIcon: true, 57 | }, 58 | }; 59 | 60 | export const Value: Story = { 61 | render: (args) => { 62 | const [value, setValue] = useState(new Date()); 63 | return ; 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /stories/SimpleTimePicker.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Source } from '@storybook/blocks'; 3 | import { fn } from '@storybook/test'; 4 | 5 | import { SimpleTimePicker } from '../components/simple-time-picker'; 6 | 7 | import { addHours, subHours, format } from 'date-fns'; 8 | 9 | import '../app/globals.css'; 10 | import { useState } from 'react'; 11 | 12 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 13 | const meta = { 14 | title: 'SimpleTimePicker', 15 | component: SimpleTimePicker, 16 | parameters: { 17 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 18 | }, 19 | tags: ['autodocs'], 20 | // More on argTypes: https://storybook.js.org/docs/api/argtypes 21 | argTypes: { 22 | // backgroundColor: { control: 'color' }, 23 | value: { control: 'date' }, 24 | disabled: { control: 'boolean' }, 25 | use12HourFormat: { control: 'boolean' }, 26 | min: { control: 'date' }, 27 | max: { control: 'date' }, 28 | }, 29 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 30 | args: { 31 | onChange: fn(), 32 | }, 33 | decorators: [ 34 | (Story, info) => ( 35 |
36 | 37 |
38 | ), 39 | ], 40 | render: (args) => { 41 | const [value, setValue] = useState(args.value || new Date()); 42 | return setValue(date)} />; 43 | }, 44 | } satisfies Meta; 45 | 46 | export default meta; 47 | type Story = StoryObj; 48 | 49 | export const Default: Story = { 50 | args: { 51 | value: new Date(), 52 | }, 53 | }; 54 | 55 | export const _12HourFormat: Story = { 56 | args: { 57 | value: new Date(), 58 | use12HourFormat: true, 59 | }, 60 | }; 61 | 62 | export const Disabled: Story = { 63 | args: { 64 | value: new Date(), 65 | disabled: true, 66 | }, 67 | }; 68 | 69 | export const MinMax: Story = { 70 | name: 'Time Selection Limits (Min/Max)', 71 | args: { 72 | value: new Date('2024-10-19T13:00:00'), 73 | min: new Date('2024-10-19T10:22:33'), 74 | max: new Date('2024-10-19T14:44:55'), 75 | }, 76 | render: (args) => { 77 | const [value, setValue] = useState(args.value || new Date()); 78 | return ( 79 |
80 |
Min: {format(args.min!, 'hh:mm:ss a')}
81 |
Max: {format(args.max!, 'hh:mm:ss a')}
82 | setValue(date)} /> 83 |
84 | ); 85 | }, 86 | }; -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DateTimePicker } from '@/components/datetime-picker'; 4 | import { Button } from '@/components/ui/button'; 5 | import { useMemo, useState } from 'react'; 6 | import { subYears, addYears } from 'date-fns'; 7 | import { Label } from '@/components/ui/label'; 8 | import { SimpleTimePicker } from '@/components/simple-time-picker'; 9 | import { format } from 'date-fns'; 10 | export default function Home() { 11 | const [date, setDate] = useState(undefined); 12 | 13 | const [date2, setDate2] = useState(new Date()); 14 | const minDate = useMemo(() => subYears(new Date(), 2), []); 15 | const maxDate = useMemo(() => addYears(new Date(), 2), []); 16 | 17 | const [time, setTime] = useState(new Date()); 18 | 19 | return ( 20 |
21 |
22 |
23 | 29 | View on GitHub 30 | 31 |

Datetime Picker

32 | 33 |

Date Picker

34 | 35 |

With timezone = UTC

36 | 37 |

Disabled

38 | 39 |

Custom trigger

40 | } 44 | /> 45 |

With min and max date

46 | 47 | 48 | 49 |

Simple Time Picker

50 | 51 |
52 |
53 |
54 | ); 55 | } 56 | function addDays(arg0: Date, arg1: number): any { 57 | throw new Error('Function not implemented.'); 58 | } 59 | 60 | function subDays(arg0: Date, arg1: number): any { 61 | throw new Error('Function not implemented.'); 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shadcn Datetime Picker 2 | 3 | ![Simple DateTime Picker](images/simple.png) 4 | 5 | ## Overview 6 | 7 | Shadcn Datetime Picker: The Ultimate React Component for Date and Time Selection 8 | 9 | Shadcn Datetime Picker is a powerful and fully customizable component that simplifies date and time selection in React applications built with the Shadcn UI framework. With advanced features designed to improve user experience, this datetime picker offers seamless integration and a responsive, user-friendly interface. Whether you need a robust datetime, date, or time picker, Shadcn Datetime Picker provides the flexibility and functionality needed for modern applications. 10 | 11 | **Key Features**: 12 | - **Datetime Picker**: Select both date and time in a single intuitive component. 13 | - **Date Picker**: A standalone date selector for quick date inputs. 14 | - **Time Picker**: Easily choose times with a simple interface. 15 | - **Timezone Support**: Display and handle dates across different timezones. 16 | - **Month/Year Selection**: Choose months and years directly for easier navigation. 17 | - **Min/Max Date**: Restrict selectable dates within a defined range. 18 | 19 | This component is designed to be lightweight, responsive, and highly customizable, making it a must-have for any project using Shadcn UI. Improve the user experience of your React apps with a reliable and flexible datetime picker solution. 20 | 21 | Demo: https://shadcn-datetime-picker-pro.vercel.app/ 22 | 23 | ### Demo DateTime Picker Input 24 | 25 | https://shadcn-datetime-picker-pro.vercel.app/?path=/story/datetimepicker--date-time-input-picker-in-form 26 | 27 | https://github.com/user-attachments/assets/1a14076d-cff7-4068-af10-d61a2ff9284b 28 | 29 | ### Installation 30 | 31 | To install the Shadcn Datetime Picker, follow these steps: 32 | 33 | 1. **Install Shadcn dependencies** 34 | ```bash 35 | npx shadcn@latest add button dropdown-menu input label popover select scroll-area 36 | ``` 37 | 38 | 2. **Install react-day-picker** 39 | ```bash 40 | yarn add react-day-picker@^9 41 | ``` 42 | 43 | 3. **Copy and paste** below codes into your project. 44 | - [datetime-picker.tsx](./components/datetime-picker.tsx) 45 | - [datetime-input.tsx](./components/datetime-input.tsx) 46 | - [simple-time-picker.tsx](./components/simple-time-picker.tsx) 47 | 48 | ### Usage 49 | 50 | #### 1. Simple DateTime Picker 51 | ```tsx 52 | import { DateTimePicker } from '@/components/datetime-picker'; 53 | 54 | export default function Home() { 55 | const [date, setDate] = useState(undefined); 56 | return ; 57 | } 58 | ``` 59 | 60 | #### 2. DateTime Picker with Timezone 61 | ```tsx 62 | import { DateTimePicker } from '@/components/datetime-picker'; 63 | 64 | export default function Home() { 65 | const [date, setDate] = useState(undefined); 66 | return ; 67 | } 68 | ``` 69 | 70 | #### 3. Custom Trigger for DateTime Picker 71 | ```tsx 72 | import { DateTimePicker } from '@/components/datetime-picker'; 73 | 74 | export default function Home() { 75 | const [date, setDate] = useState(undefined); 76 | return ( 77 | } 81 | /> 82 | ); 83 | } 84 | ``` 85 | 86 | #### 4. With min and max date 87 | ```tsx 88 | import { DateTimePicker } from '@/components/datetime-picker'; 89 | 90 | export default function Home() { 91 | const [date, setDate] = useState(undefined); 92 | const minDate = useMemo(() => subHours(new Date(), 2), []); 93 | const maxDate = useMemo(() => addMonths(new Date(), 2), []); 94 | return ; 95 | } 96 | ``` 97 | 98 | ### Contributing 99 | 100 | We welcome contributions! Please feel free to submit a pull request or open an issue for any suggestions or improvements. 101 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { cn } from "@/lib/utils" 6 | import { Cross2Icon } from "@radix-ui/react-icons" 7 | 8 | const Dialog = DialogPrimitive.Root 9 | 10 | const DialogTrigger = DialogPrimitive.Trigger 11 | 12 | const DialogPortal = DialogPrimitive.Portal 13 | 14 | const DialogClose = DialogPrimitive.Close 15 | 16 | const DialogOverlay = React.forwardRef< 17 | React.ElementRef, 18 | React.ComponentPropsWithoutRef 19 | >(({ className, ...props }, ref) => ( 20 | 28 | )) 29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 30 | 31 | const DialogContent = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef 34 | >(({ className, children, ...props }, ref) => ( 35 | 36 | 37 | 45 | {children} 46 | 47 | 48 | Close 49 | 50 | 51 | 52 | )) 53 | DialogContent.displayName = DialogPrimitive.Content.displayName 54 | 55 | const DialogHeader = ({ 56 | className, 57 | ...props 58 | }: React.HTMLAttributes) => ( 59 |
66 | ) 67 | DialogHeader.displayName = "DialogHeader" 68 | 69 | const DialogFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
80 | ) 81 | DialogFooter.displayName = "DialogFooter" 82 | 83 | const DialogTitle = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )) 96 | DialogTitle.displayName = DialogPrimitive.Title.displayName 97 | 98 | const DialogDescription = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 107 | )) 108 | DialogDescription.displayName = DialogPrimitive.Description.displayName 109 | 110 | export { 111 | Dialog, 112 | DialogPortal, 113 | DialogOverlay, 114 | DialogTrigger, 115 | DialogClose, 116 | DialogContent, 117 | DialogHeader, 118 | DialogFooter, 119 | DialogTitle, 120 | DialogDescription, 121 | } 122 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |