├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── eslint.config.mjs
├── image-1.png
├── image-2.png
├── image.png
├── next.config.mjs
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── github.svg
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── datetime-picker
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── input-typewriter
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── app-sidebar.tsx
│ ├── date-time-picker-v1.tsx
│ ├── date-time-picker-v2.tsx
│ ├── types.d.ts
│ ├── typewriter.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── sonner.tsx
│ │ └── tooltip.tsx
├── hooks
│ └── use-mobile.tsx
└── lib
│ └── utils.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
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 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | # local files
44 | bun.lockb
45 | package-lock.json
46 | yarn.lock
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Saad
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shadcn UI - Datetime Picker
2 |
3 | This project provides a beautifully crafted datetime picker component built using the Shadcn UI. It offers an intuitive interface for selecting dates and times in React applications.
4 |
5 | ## Features
6 |
7 | - **Date and Time Selection**: Allows users to pick both date and time seamlessly.
8 | - **Customizable Layout**: Easily adaptable to fit various design requirements.
9 | - **Responsive Design**: Ensures optimal user experience across different devices.
10 |
11 | ## Installation
12 |
13 | To integrate the Shadcn Datetime Picker into your project, follow these steps:
14 |
15 | 1. **Install Packages**:
16 |
17 | ```bash
18 | npm install
19 | ```
20 |
21 | 2. **Start the Development Server**:
22 |
23 | ```bash
24 | npm start
25 | ```
26 |
27 | ## Contributing
28 |
29 | We welcome contributions! If you have suggestions or improvements, please feel free to submit a pull request or open an issue.
30 |
31 | ## License
32 |
33 | This project is licensed under the MIT License.
34 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
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.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/image-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maliksidk19/shadcn-datetime-picker/ae6e76b232c1de4e0cbcd87d86c109bcf2022d3e/image-1.png
--------------------------------------------------------------------------------
/image-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maliksidk19/shadcn-datetime-picker/ae6e76b232c1de4e0cbcd87d86c109bcf2022d3e/image-2.png
--------------------------------------------------------------------------------
/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maliksidk19/shadcn-datetime-picker/ae6e76b232c1de4e0cbcd87d86c109bcf2022d3e/image.png
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: false,
4 | };
5 |
6 | export default nextConfig;
7 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | reactStrictMode: false,
6 | };
7 |
8 | export default nextConfig;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadcn-datetime",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^4.0.0",
13 | "@radix-ui/react-dialog": "^1.1.6",
14 | "@radix-ui/react-label": "^2.1.2",
15 | "@radix-ui/react-popover": "^1.1.6",
16 | "@radix-ui/react-scroll-area": "^1.2.3",
17 | "@radix-ui/react-select": "^2.1.6",
18 | "@radix-ui/react-separator": "^1.1.2",
19 | "@radix-ui/react-slot": "^1.1.2",
20 | "@radix-ui/react-tooltip": "^1.1.8",
21 | "class-variance-authority": "^0.7.1",
22 | "clsx": "^2.1.1",
23 | "date-fns": "^4.1.0",
24 | "lucide-react": "^0.475.0",
25 | "next": "15.1.7",
26 | "next-themes": "^0.4.4",
27 | "react": "^19.0.0",
28 | "react-day-picker": "^9.5.1",
29 | "react-dom": "^19.0.0",
30 | "react-hook-form": "^7.54.2",
31 | "sonner": "^1.7.4",
32 | "tailwind-merge": "^3.0.1",
33 | "tailwindcss": "3.4.1",
34 | "tailwindcss-animate": "^1.0.7",
35 | "typewriter-effect": "^2.21.0",
36 | "zod": "^3.24.2"
37 | },
38 | "devDependencies": {
39 | "@eslint/eslintrc": "^3.2.0",
40 | "@types/node": "^22.13.4",
41 | "@types/react": "^19.0.8",
42 | "@types/react-dom": "^19.0.3",
43 | "eslint": "^9.20.1",
44 | "eslint-config-next": "15.1.7",
45 | "postcss": "^8.5.2",
46 | "typescript": "^5.7.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/datetime-picker/page.tsx:
--------------------------------------------------------------------------------
1 | import { DatetimePickerV1 } from "@/components/date-time-picker-v1";
2 | import { DateTimePickerV2 } from "@/components/date-time-picker-v2";
3 |
4 | const DateTimePickerComp = () => {
5 | return (
6 |
7 |
Datetime Picker V1
8 |
9 | Datetime Picker V2
10 |
11 |
12 | );
13 | };
14 |
15 | export default DateTimePickerComp;
16 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maliksidk19/shadcn-datetime-picker/ae6e76b232c1de4e0cbcd87d86c109bcf2022d3e/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 222.2 84% 4.9%;
15 | --card: 0 0% 100%;
16 | --card-foreground: 222.2 84% 4.9%;
17 | --popover: 0 0% 100%;
18 | --popover-foreground: 222.2 84% 4.9%;
19 | --primary: 222.2 47.4% 11.2%;
20 | --primary-foreground: 210 40% 98%;
21 | --secondary: 210 40% 96.1%;
22 | --secondary-foreground: 222.2 47.4% 11.2%;
23 | --muted: 210 40% 96.1%;
24 | --muted-foreground: 215.4 16.3% 46.9%;
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 | --destructive: 0 84.2% 60.2%;
28 | --destructive-foreground: 210 40% 98%;
29 | --border: 214.3 31.8% 91.4%;
30 | --input: 214.3 31.8% 91.4%;
31 | --ring: 222.2 84% 4.9%;
32 | --radius: 0.5rem;
33 | --chart-1: 12 76% 61%;
34 | --chart-2: 173 58% 39%;
35 | --chart-3: 197 37% 24%;
36 | --chart-4: 43 74% 66%;
37 | --chart-5: 27 87% 67%;
38 | --sidebar-background: 0 0% 98%;
39 | --sidebar-foreground: 240 5.3% 26.1%;
40 | --sidebar-primary: 240 5.9% 10%;
41 | --sidebar-primary-foreground: 0 0% 98%;
42 | --sidebar-accent: 240 4.8% 95.9%;
43 | --sidebar-accent-foreground: 240 5.9% 10%;
44 | --sidebar-border: 220 13% 91%;
45 | --sidebar-ring: 217.2 91.2% 59.8%;
46 | }
47 |
48 | .dark {
49 | --background: 222.2 84% 4.9%;
50 | --foreground: 210 40% 98%;
51 | --card: 222.2 84% 4.9%;
52 | --card-foreground: 210 40% 98%;
53 | --popover: 222.2 84% 4.9%;
54 | --popover-foreground: 210 40% 98%;
55 | --primary: 210 40% 98%;
56 | --primary-foreground: 222.2 47.4% 11.2%;
57 | --secondary: 217.2 32.6% 17.5%;
58 | --secondary-foreground: 210 40% 98%;
59 | --muted: 217.2 32.6% 17.5%;
60 | --muted-foreground: 215 20.2% 65.1%;
61 | --accent: 217.2 32.6% 17.5%;
62 | --accent-foreground: 210 40% 98%;
63 | --destructive: 0 62.8% 30.6%;
64 | --destructive-foreground: 210 40% 98%;
65 | --border: 217.2 32.6% 17.5%;
66 | --input: 217.2 32.6% 17.5%;
67 | --ring: 212.7 26.8% 83.9%;
68 | --chart-1: 220 70% 50%;
69 | --chart-2: 160 60% 45%;
70 | --chart-3: 30 80% 55%;
71 | --chart-4: 280 65% 60%;
72 | --chart-5: 340 75% 55%;
73 | --sidebar-background: 240 5.9% 10%;
74 | --sidebar-foreground: 240 4.8% 95.9%;
75 | --sidebar-primary: 224.3 76.3% 48%;
76 | --sidebar-primary-foreground: 0 0% 100%;
77 | --sidebar-accent: 240 3.7% 15.9%;
78 | --sidebar-accent-foreground: 240 4.8% 95.9%;
79 | --sidebar-border: 240 3.7% 15.9%;
80 | --sidebar-ring: 217.2 91.2% 59.8%;
81 | }
82 | }
83 |
84 | @layer base {
85 | * {
86 | @apply border-border;
87 | }
88 | body {
89 | @apply bg-background text-foreground;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/app/input-typewriter/page.tsx:
--------------------------------------------------------------------------------
1 | import TypeWriter from "@/components/typewriter";
2 |
3 | const TypeWriterInput = () => {
4 | return (
5 |
6 | TypeWriter Input
7 |
8 |
9 | );
10 | };
11 |
12 | export default TypeWriterInput;
13 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { Toaster } from "sonner";
5 | import { cn } from "@/lib/utils";
6 | import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
7 | import AppSidebar from "@/components/app-sidebar";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | export const metadata: Metadata = {
12 | title: "Shadcn Datetime Picker",
13 | description:
14 | "Shadcn Datetime Picker is a simple and easy-to-use datetime picker component for React. It is built with Tailwind CSS and Shadcn UI.",
15 | };
16 |
17 | export default function RootLayout({
18 | children,
19 | }: Readonly<{
20 | children: React.ReactNode;
21 | }>) {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 | {children}
30 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | const HomePage = () => {
2 | return (
3 |
4 |
Welcome to Shadcn Components
5 |
6 | );
7 | };
8 |
9 | export default HomePage;
10 |
--------------------------------------------------------------------------------
/src/components/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import {
3 | Sidebar,
4 | SidebarContent,
5 | SidebarFooter,
6 | SidebarGroup,
7 | SidebarGroupContent,
8 | SidebarGroupLabel,
9 | SidebarHeader,
10 | SidebarMenu,
11 | SidebarMenuButton,
12 | SidebarMenuItem,
13 | } from "./ui/sidebar";
14 | import Image from "next/image";
15 |
16 | const AppSidebar = () => {
17 | return (
18 |
19 |
20 | Shadcn Components
21 |
22 |
23 |
24 |
25 | Components
26 |
27 |
28 |
29 |
30 | Datetime Picker
31 |
32 |
33 |
34 |
35 | Input Typewriter
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Made with 💖 by{" "}
46 |
51 | Saad
52 |
53 |
54 |
55 |
56 |
61 | Give a star on GitHub
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default AppSidebar;
70 |
--------------------------------------------------------------------------------
/src/components/date-time-picker-v1.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { CalendarIcon } from "lucide-react";
5 | import { format } from "date-fns";
6 | import { useRef, useState } from "react";
7 | import { useForm } from "react-hook-form";
8 | import { z } from "zod";
9 |
10 | import { cn } from "../lib/utils";
11 | import { Button } from "./ui/button";
12 | import { Calendar } from "./ui/calendar";
13 | import {
14 | Form,
15 | FormControl,
16 | FormDescription,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from "./ui/form";
22 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
23 | import { ScrollArea } from "./ui/scroll-area";
24 | import { toast } from "sonner";
25 |
26 | const FormSchema = z.object({
27 | datetime: z.date({
28 | required_error: "Date & time is required!",
29 | }),
30 | });
31 |
32 | export function DatetimePickerV1() {
33 | const [isOpen, setIsOpen] = useState(false);
34 | const [time, setTime] = useState("05:00");
35 | const [date, setDate] = useState(new Date()); // Default button height
36 |
37 | const calendarRef = useRef(null);
38 | const form = useForm>({
39 | resolver: zodResolver(FormSchema),
40 | });
41 |
42 | async function onSubmit(data: z.infer) {
43 | toast.success(`Meeting at: ${format(data.datetime, "PPP, p")}`);
44 | }
45 |
46 | return (
47 |
149 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/src/components/date-time-picker-v2.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { CalendarIcon } from "lucide-react";
5 | import { format } from "date-fns";
6 | import { useForm } from "react-hook-form";
7 | import { z } from "zod";
8 |
9 | import { cn } from "../lib/utils";
10 | import { Button } from "./ui/button";
11 | import { Calendar } from "./ui/calendar";
12 | import {
13 | Form,
14 | FormControl,
15 | FormDescription,
16 | FormField,
17 | FormItem,
18 | FormLabel,
19 | FormMessage,
20 | } from "./ui/form";
21 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
22 | import { useState } from "react";
23 | import {
24 | Select,
25 | SelectContent,
26 | SelectItem,
27 | SelectTrigger,
28 | SelectValue,
29 | } from "./ui/select";
30 | import { ScrollArea } from "./ui/scroll-area";
31 | import { toast } from "sonner";
32 |
33 | const FormSchema = z.object({
34 | datetime: z.date({
35 | required_error: "Date & time is required!.",
36 | }),
37 | });
38 |
39 | export function DateTimePickerV2() {
40 | const [isOpen, setIsOpen] = useState(false);
41 | const [time, setTime] = useState("05:00");
42 | const [date, setDate] = useState(null);
43 | const form = useForm>({
44 | resolver: zodResolver(FormSchema),
45 | });
46 |
47 | async function onSubmit(data: z.infer) {
48 | toast.success(`Meeting at: ${format(data.datetime, "PPP, p")}`);
49 | }
50 |
51 | return (
52 | <>
53 |
160 |
161 | >
162 | );
163 | }
164 |
--------------------------------------------------------------------------------
/src/components/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module "typewriter-effect";
2 | declare module "typewriter-effect/dist/core";
3 |
--------------------------------------------------------------------------------
/src/components/typewriter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Form } from "@/components/ui/form";
4 | import { useForm } from "react-hook-form";
5 | import { useEffect, useRef } from "react";
6 | import Typewriter from "typewriter-effect/dist/core";
7 | import { z } from "zod";
8 | import { zodResolver } from "@hookform/resolvers/zod";
9 | import {
10 | FormField,
11 | FormItem,
12 | FormControl,
13 | FormDescription,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form";
17 | import { Input } from "@/components/ui/input";
18 | import { Button } from "@/components/ui/button";
19 | import { toast } from "sonner";
20 |
21 | const TypeWriter = () => {
22 | const inputFormRef = useRef(null);
23 |
24 | useEffect(() => {
25 | const typewriter: typeof Typewriter | null = null;
26 |
27 | if (inputFormRef.current) {
28 | const customNodeCreator = function (character: string) {
29 | if (inputFormRef.current) {
30 | inputFormRef.current!.placeholder += character;
31 | }
32 | return null;
33 | };
34 |
35 | const onRemoveNode = function () {
36 | if (inputFormRef.current) {
37 | inputFormRef.current!.placeholder =
38 | inputFormRef.current!.placeholder.slice(0, -1);
39 | }
40 | };
41 |
42 | const typewriter = new Typewriter(null, {
43 | loop: true,
44 | delay: 20,
45 | deleteSpeed: 20,
46 | onCreateTextNode: customNodeCreator,
47 | onRemoveNode: onRemoveNode,
48 | });
49 |
50 | typewriter
51 | .typeString("Generate an image of Solar System?")
52 | .pauseFor(1000)
53 | .deleteAll(20)
54 | .typeString("Generate an image of book Shelf?")
55 | .pauseFor(1000)
56 | .start();
57 | }
58 |
59 | return () => {
60 | if (typewriter) {
61 | typewriter.stop();
62 | }
63 | };
64 | }, []);
65 |
66 | const promptSchema = z.object({
67 | prompt: z.string(),
68 | });
69 |
70 | const form = useForm>({
71 | resolver: zodResolver(promptSchema),
72 | defaultValues: {
73 | prompt: "",
74 | },
75 | });
76 |
77 | const onSubmit = async (data: z.infer) => {
78 | toast.info(data.prompt);
79 | };
80 |
81 | return (
82 |
119 |
120 | );
121 | };
122 |
123 | export default TypeWriter;
124 |
--------------------------------------------------------------------------------
/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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { DayPicker, Dropdown as DropDownDayPicker } from "react-day-picker";
5 |
6 | import { buttonVariants } from "@/components/ui/button";
7 | import { ScrollArea } from "@/components/ui/scroll-area";
8 | import {
9 | Select,
10 | SelectContent,
11 | SelectItem,
12 | SelectTrigger,
13 | SelectValue,
14 | } from "@/components/ui/select";
15 | import { cn } from "@/lib/utils";
16 |
17 | export type CalendarProps = React.ComponentProps & {
18 | captionLabelClassName?: string;
19 | dayClassName?: string;
20 | dayButtonClassName?: string;
21 | dropdownsClassName?: string;
22 | footerClassName?: string;
23 | monthClassName?: string;
24 | monthCaptionClassName?: string;
25 | monthGridClassName?: string;
26 | monthsClassName?: string;
27 | weekClassName?: string;
28 | weekdayClassName?: string;
29 | weekdaysClassName?: string;
30 | rangeEndClassName?: string;
31 | rangeMiddleClassName?: string;
32 | rangeStartClassName?: string;
33 | selectedClassName?: string;
34 | disabledClassName?: string;
35 | hiddenClassName?: string;
36 | outsideClassName?: string;
37 | todayClassName?: string;
38 | selectTriggerClassName?: string;
39 | };
40 |
41 | function Calendar({
42 | className,
43 | classNames,
44 | hideNavigation,
45 | showOutsideDays = true,
46 | components: customComponents,
47 | ...props
48 | }: CalendarProps) {
49 | const _monthsClassName = cn(
50 | "relative flex flex-col gap-4 sm:flex-row",
51 | props.monthsClassName
52 | );
53 | const _monthCaptionClassName = cn(
54 | "relative flex h-7 items-center justify-center",
55 | props.monthCaptionClassName
56 | );
57 | const _dropdownsClassName = cn(
58 | "flex items-center justify-center gap-2 w-full",
59 | hideNavigation ? "w-full" : "",
60 | props.dropdownsClassName
61 | );
62 | const _footerClassName = cn("pt-3 text-sm", props.footerClassName);
63 | const _weekdaysClassName = cn("flex", props.weekdaysClassName);
64 | const _weekdayClassName = cn(
65 | "w-9 text-sm font-normal text-muted-foreground",
66 | props.weekdayClassName
67 | );
68 | const _captionLabelClassName = cn(
69 | "truncate text-sm font-medium",
70 | props.captionLabelClassName
71 | );
72 |
73 | const _monthGridClassName = cn("mx-auto mt-4", props.monthGridClassName);
74 | const _weekClassName = cn("mt-2 flex w-max items-start", props.weekClassName);
75 | const _dayClassName = cn(
76 | "flex size-9 flex-1 items-center justify-center p-0 text-sm",
77 | props.dayClassName
78 | );
79 | const _dayButtonClassName = cn(
80 | buttonVariants({ variant: "ghost" }),
81 | "size-9 rounded-md p-0 font-normal transition-none aria-selected:opacity-100",
82 | props.dayButtonClassName
83 | );
84 |
85 | const buttonRangeClassName =
86 | "bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground";
87 | const _rangeStartClassName = cn(
88 | buttonRangeClassName,
89 | "rounded-s-md",
90 | props.rangeStartClassName
91 | );
92 | const _rangeEndClassName = cn(
93 | buttonRangeClassName,
94 | "rounded-e-md",
95 | props.rangeEndClassName
96 | );
97 | const _rangeMiddleClassName = cn(
98 | "bg-accent !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground",
99 | props.rangeMiddleClassName
100 | );
101 | const _selectedClassName = cn(
102 | "[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground",
103 | props.selectedClassName
104 | );
105 | const _todayClassName = cn(
106 | "[&>button]:bg-accent [&>button]:text-accent-foreground",
107 | props.todayClassName
108 | );
109 | const _outsideClassName = cn(
110 | "text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
111 | props.outsideClassName
112 | );
113 | const _disabledClassName = cn(
114 | "text-muted-foreground opacity-50",
115 | props.disabledClassName
116 | );
117 | const _hiddenClassName = cn("invisible flex-1", props.hiddenClassName);
118 |
119 | const Dropdown = React.useCallback(
120 | ({
121 | value,
122 | onChange,
123 | options,
124 | }: React.ComponentProps) => {
125 | const selected = options?.find((option) => option.value === value);
126 | const handleChange = (value: string) => {
127 | const changeEvent = {
128 | target: { value },
129 | } as React.ChangeEvent;
130 | onChange?.(changeEvent);
131 | };
132 | return (
133 |
156 | );
157 | },
158 | []
159 | );
160 |
161 | return (
162 |
196 | );
197 | }
198 | Calendar.displayName = "Calendar";
199 |
200 | export { Calendar };
201 |
--------------------------------------------------------------------------------
/src/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 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/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/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 PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { Check, ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ));
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ));
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName;
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ));
100 | SelectContent.displayName = SelectPrimitive.Content.displayName;
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ));
135 | SelectItem.displayName = SelectPrimitive.Item.displayName;
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ));
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | };
161 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/components/ui/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Slot } from "@radix-ui/react-slot";
5 | import { VariantProps, cva } from "class-variance-authority";
6 | import { PanelLeft } from "lucide-react";
7 |
8 | import { useIsMobile } from "@/hooks/use-mobile";
9 | import { cn } from "@/lib/utils";
10 | import { Button } from "@/components/ui/button";
11 | import { Input } from "@/components/ui/input";
12 | import { Separator } from "@/components/ui/separator";
13 | import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
14 | import { Skeleton } from "@/components/ui/skeleton";
15 | import {
16 | Tooltip,
17 | TooltipContent,
18 | TooltipProvider,
19 | TooltipTrigger,
20 | } from "@/components/ui/tooltip";
21 |
22 | const SIDEBAR_COOKIE_NAME = "sidebar_state";
23 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
24 | const SIDEBAR_WIDTH = "16rem";
25 | const SIDEBAR_WIDTH_MOBILE = "18rem";
26 | const SIDEBAR_WIDTH_ICON = "3rem";
27 | const SIDEBAR_KEYBOARD_SHORTCUT = "b";
28 |
29 | type SidebarContext = {
30 | state: "expanded" | "collapsed";
31 | open: boolean;
32 | setOpen: (open: boolean) => void;
33 | openMobile: boolean;
34 | setOpenMobile: (open: boolean) => void;
35 | isMobile: boolean;
36 | toggleSidebar: () => void;
37 | };
38 |
39 | const SidebarContext = React.createContext(null);
40 |
41 | function useSidebar() {
42 | const context = React.useContext(SidebarContext);
43 | if (!context) {
44 | throw new Error("useSidebar must be used within a SidebarProvider.");
45 | }
46 |
47 | return context;
48 | }
49 |
50 | const SidebarProvider = React.forwardRef<
51 | HTMLDivElement,
52 | React.ComponentProps<"div"> & {
53 | defaultOpen?: boolean;
54 | open?: boolean;
55 | onOpenChange?: (open: boolean) => void;
56 | }
57 | >(
58 | (
59 | {
60 | defaultOpen = true,
61 | open: openProp,
62 | onOpenChange: setOpenProp,
63 | className,
64 | style,
65 | children,
66 | ...props
67 | },
68 | ref
69 | ) => {
70 | const isMobile = useIsMobile();
71 | const [openMobile, setOpenMobile] = React.useState(false);
72 |
73 | // This is the internal state of the sidebar.
74 | // We use openProp and setOpenProp for control from outside the component.
75 | const [_open, _setOpen] = React.useState(defaultOpen);
76 | const open = openProp ?? _open;
77 | const setOpen = React.useCallback(
78 | (value: boolean | ((value: boolean) => boolean)) => {
79 | const openState = typeof value === "function" ? value(open) : value;
80 | if (setOpenProp) {
81 | setOpenProp(openState);
82 | } else {
83 | _setOpen(openState);
84 | }
85 |
86 | // This sets the cookie to keep the sidebar state.
87 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
88 | },
89 | [setOpenProp, open]
90 | );
91 |
92 | // Helper to toggle the sidebar.
93 | const toggleSidebar = React.useCallback(() => {
94 | return isMobile
95 | ? setOpenMobile((open) => !open)
96 | : setOpen((open) => !open);
97 | }, [isMobile, setOpen, setOpenMobile]);
98 |
99 | // Adds a keyboard shortcut to toggle the sidebar.
100 | React.useEffect(() => {
101 | const handleKeyDown = (event: KeyboardEvent) => {
102 | if (
103 | event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
104 | (event.metaKey || event.ctrlKey)
105 | ) {
106 | event.preventDefault();
107 | toggleSidebar();
108 | }
109 | };
110 |
111 | window.addEventListener("keydown", handleKeyDown);
112 | return () => window.removeEventListener("keydown", handleKeyDown);
113 | }, [toggleSidebar]);
114 |
115 | // We add a state so that we can do data-state="expanded" or "collapsed".
116 | // This makes it easier to style the sidebar with Tailwind classes.
117 | const state = open ? "expanded" : "collapsed";
118 |
119 | const contextValue = React.useMemo(
120 | () => ({
121 | state,
122 | open,
123 | setOpen,
124 | isMobile,
125 | openMobile,
126 | setOpenMobile,
127 | toggleSidebar,
128 | }),
129 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
130 | );
131 |
132 | return (
133 |
134 |
135 |
150 | {children}
151 |
152 |
153 |
154 | );
155 | }
156 | );
157 | SidebarProvider.displayName = "SidebarProvider";
158 |
159 | const Sidebar = React.forwardRef<
160 | HTMLDivElement,
161 | React.ComponentProps<"div"> & {
162 | side?: "left" | "right";
163 | variant?: "sidebar" | "floating" | "inset";
164 | collapsible?: "offcanvas" | "icon" | "none";
165 | }
166 | >(
167 | (
168 | {
169 | side = "left",
170 | variant = "sidebar",
171 | collapsible = "offcanvas",
172 | className,
173 | children,
174 | ...props
175 | },
176 | ref
177 | ) => {
178 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
179 |
180 | if (collapsible === "none") {
181 | return (
182 |
190 | {children}
191 |
192 | );
193 | }
194 |
195 | if (isMobile) {
196 | return (
197 |
198 |
209 | Shadcn Components
210 | {children}
211 |
212 |
213 | );
214 | }
215 |
216 | return (
217 |
225 | {/* This is what handles the sidebar gap on desktop */}
226 |
236 |
250 |
254 | {children}
255 |
256 |
257 |
258 | );
259 | }
260 | );
261 | Sidebar.displayName = "Sidebar";
262 |
263 | const SidebarTrigger = React.forwardRef<
264 | React.ElementRef,
265 | React.ComponentProps
266 | >(({ className, onClick, ...props }, ref) => {
267 | const { toggleSidebar } = useSidebar();
268 |
269 | return (
270 |
285 | );
286 | });
287 | SidebarTrigger.displayName = "SidebarTrigger";
288 |
289 | const SidebarRail = React.forwardRef<
290 | HTMLButtonElement,
291 | React.ComponentProps<"button">
292 | >(({ className, ...props }, ref) => {
293 | const { toggleSidebar } = useSidebar();
294 |
295 | return (
296 |
314 | );
315 | });
316 | SidebarRail.displayName = "SidebarRail";
317 |
318 | const SidebarInset = React.forwardRef<
319 | HTMLDivElement,
320 | React.ComponentProps<"main">
321 | >(({ className, ...props }, ref) => {
322 | return (
323 |
332 | );
333 | });
334 | SidebarInset.displayName = "SidebarInset";
335 |
336 | const SidebarInput = React.forwardRef<
337 | React.ElementRef,
338 | React.ComponentProps
339 | >(({ className, ...props }, ref) => {
340 | return (
341 |
350 | );
351 | });
352 | SidebarInput.displayName = "SidebarInput";
353 |
354 | const SidebarHeader = React.forwardRef<
355 | HTMLDivElement,
356 | React.ComponentProps<"div">
357 | >(({ className, ...props }, ref) => {
358 | return (
359 |
365 | );
366 | });
367 | SidebarHeader.displayName = "SidebarHeader";
368 |
369 | const SidebarFooter = React.forwardRef<
370 | HTMLDivElement,
371 | React.ComponentProps<"div">
372 | >(({ className, ...props }, ref) => {
373 | return (
374 |
380 | );
381 | });
382 | SidebarFooter.displayName = "SidebarFooter";
383 |
384 | const SidebarSeparator = React.forwardRef<
385 | React.ElementRef,
386 | React.ComponentProps
387 | >(({ className, ...props }, ref) => {
388 | return (
389 |
395 | );
396 | });
397 | SidebarSeparator.displayName = "SidebarSeparator";
398 |
399 | const SidebarContent = React.forwardRef<
400 | HTMLDivElement,
401 | React.ComponentProps<"div">
402 | >(({ className, ...props }, ref) => {
403 | return (
404 |
413 | );
414 | });
415 | SidebarContent.displayName = "SidebarContent";
416 |
417 | const SidebarGroup = React.forwardRef<
418 | HTMLDivElement,
419 | React.ComponentProps<"div">
420 | >(({ className, ...props }, ref) => {
421 | return (
422 |
428 | );
429 | });
430 | SidebarGroup.displayName = "SidebarGroup";
431 |
432 | const SidebarGroupLabel = React.forwardRef<
433 | HTMLDivElement,
434 | React.ComponentProps<"div"> & { asChild?: boolean }
435 | >(({ className, asChild = false, ...props }, ref) => {
436 | const Comp = asChild ? Slot : "div";
437 |
438 | return (
439 | svg]:size-4 [&>svg]:shrink-0",
444 | "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
445 | className
446 | )}
447 | {...props}
448 | />
449 | );
450 | });
451 | SidebarGroupLabel.displayName = "SidebarGroupLabel";
452 |
453 | const SidebarGroupAction = React.forwardRef<
454 | HTMLButtonElement,
455 | React.ComponentProps<"button"> & { asChild?: boolean }
456 | >(({ className, asChild = false, ...props }, ref) => {
457 | const Comp = asChild ? Slot : "button";
458 |
459 | return (
460 | svg]:size-4 [&>svg]:shrink-0",
465 | // Increases the hit area of the button on mobile.
466 | "after:absolute after:-inset-2 after:md:hidden",
467 | "group-data-[collapsible=icon]:hidden",
468 | className
469 | )}
470 | {...props}
471 | />
472 | );
473 | });
474 | SidebarGroupAction.displayName = "SidebarGroupAction";
475 |
476 | const SidebarGroupContent = React.forwardRef<
477 | HTMLDivElement,
478 | React.ComponentProps<"div">
479 | >(({ className, ...props }, ref) => (
480 |
486 | ));
487 | SidebarGroupContent.displayName = "SidebarGroupContent";
488 |
489 | const SidebarMenu = React.forwardRef<
490 | HTMLUListElement,
491 | React.ComponentProps<"ul">
492 | >(({ className, ...props }, ref) => (
493 |
499 | ));
500 | SidebarMenu.displayName = "SidebarMenu";
501 |
502 | const SidebarMenuItem = React.forwardRef<
503 | HTMLLIElement,
504 | React.ComponentProps<"li">
505 | >(({ className, ...props }, ref) => (
506 |
512 | ));
513 | SidebarMenuItem.displayName = "SidebarMenuItem";
514 |
515 | const sidebarMenuButtonVariants = cva(
516 | "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
517 | {
518 | variants: {
519 | variant: {
520 | default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
521 | outline:
522 | "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
523 | },
524 | size: {
525 | default: "h-8 text-sm",
526 | sm: "h-7 text-xs",
527 | lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
528 | },
529 | },
530 | defaultVariants: {
531 | variant: "default",
532 | size: "default",
533 | },
534 | }
535 | );
536 |
537 | const SidebarMenuButton = React.forwardRef<
538 | HTMLButtonElement,
539 | React.ComponentProps<"button"> & {
540 | asChild?: boolean;
541 | isActive?: boolean;
542 | tooltip?: string | React.ComponentProps;
543 | } & VariantProps
544 | >(
545 | (
546 | {
547 | asChild = false,
548 | isActive = false,
549 | variant = "default",
550 | size = "default",
551 | tooltip,
552 | className,
553 | ...props
554 | },
555 | ref
556 | ) => {
557 | const Comp = asChild ? Slot : "button";
558 | const { isMobile, state } = useSidebar();
559 |
560 | const button = (
561 |
569 | );
570 |
571 | if (!tooltip) {
572 | return button;
573 | }
574 |
575 | if (typeof tooltip === "string") {
576 | tooltip = {
577 | children: tooltip,
578 | };
579 | }
580 |
581 | return (
582 |
583 | {button}
584 |
590 |
591 | );
592 | }
593 | );
594 | SidebarMenuButton.displayName = "SidebarMenuButton";
595 |
596 | const SidebarMenuAction = React.forwardRef<
597 | HTMLButtonElement,
598 | React.ComponentProps<"button"> & {
599 | asChild?: boolean;
600 | showOnHover?: boolean;
601 | }
602 | >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
603 | const Comp = asChild ? Slot : "button";
604 |
605 | return (
606 | svg]:size-4 [&>svg]:shrink-0",
611 | // Increases the hit area of the button on mobile.
612 | "after:absolute after:-inset-2 after:md:hidden",
613 | "peer-data-[size=sm]/menu-button:top-1",
614 | "peer-data-[size=default]/menu-button:top-1.5",
615 | "peer-data-[size=lg]/menu-button:top-2.5",
616 | "group-data-[collapsible=icon]:hidden",
617 | showOnHover &&
618 | "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
619 | className
620 | )}
621 | {...props}
622 | />
623 | );
624 | });
625 | SidebarMenuAction.displayName = "SidebarMenuAction";
626 |
627 | const SidebarMenuBadge = React.forwardRef<
628 | HTMLDivElement,
629 | React.ComponentProps<"div">
630 | >(({ className, ...props }, ref) => (
631 |
645 | ));
646 | SidebarMenuBadge.displayName = "SidebarMenuBadge";
647 |
648 | const SidebarMenuSkeleton = React.forwardRef<
649 | HTMLDivElement,
650 | React.ComponentProps<"div"> & {
651 | showIcon?: boolean;
652 | }
653 | >(({ className, showIcon = false, ...props }, ref) => {
654 | // Random width between 50 to 90%.
655 | const width = React.useMemo(() => {
656 | return `${Math.floor(Math.random() * 40) + 50}%`;
657 | }, []);
658 |
659 | return (
660 |
666 | {showIcon && (
667 |
671 | )}
672 |
681 |
682 | );
683 | });
684 | SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
685 |
686 | const SidebarMenuSub = React.forwardRef<
687 | HTMLUListElement,
688 | React.ComponentProps<"ul">
689 | >(({ className, ...props }, ref) => (
690 |
700 | ));
701 | SidebarMenuSub.displayName = "SidebarMenuSub";
702 |
703 | const SidebarMenuSubItem = React.forwardRef<
704 | HTMLLIElement,
705 | React.ComponentProps<"li">
706 | >(({ ...props }, ref) => );
707 | SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
708 |
709 | const SidebarMenuSubButton = React.forwardRef<
710 | HTMLAnchorElement,
711 | React.ComponentProps<"a"> & {
712 | asChild?: boolean;
713 | size?: "sm" | "md";
714 | isActive?: boolean;
715 | }
716 | >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
717 | const Comp = asChild ? Slot : "a";
718 |
719 | return (
720 | span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
727 | "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
728 | size === "sm" && "text-xs",
729 | size === "md" && "text-sm",
730 | "group-data-[collapsible=icon]:hidden",
731 | className
732 | )}
733 | {...props}
734 | />
735 | );
736 | });
737 | SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
738 |
739 | export {
740 | Sidebar,
741 | SidebarContent,
742 | SidebarFooter,
743 | SidebarGroup,
744 | SidebarGroupAction,
745 | SidebarGroupContent,
746 | SidebarGroupLabel,
747 | SidebarHeader,
748 | SidebarInput,
749 | SidebarInset,
750 | SidebarMenu,
751 | SidebarMenuAction,
752 | SidebarMenuBadge,
753 | SidebarMenuButton,
754 | SidebarMenuItem,
755 | SidebarMenuSkeleton,
756 | SidebarMenuSub,
757 | SidebarMenuSubButton,
758 | SidebarMenuSubItem,
759 | SidebarProvider,
760 | SidebarRail,
761 | SidebarSeparator,
762 | SidebarTrigger,
763 | useSidebar,
764 | };
765 |
--------------------------------------------------------------------------------
/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/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/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 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/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 | sidebar: {
54 | DEFAULT: 'hsl(var(--sidebar-background))',
55 | foreground: 'hsl(var(--sidebar-foreground))',
56 | primary: 'hsl(var(--sidebar-primary))',
57 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
58 | accent: 'hsl(var(--sidebar-accent))',
59 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
60 | border: 'hsl(var(--sidebar-border))',
61 | ring: 'hsl(var(--sidebar-ring))'
62 | }
63 | },
64 | borderRadius: {
65 | lg: 'var(--radius)',
66 | md: 'calc(var(--radius) - 2px)',
67 | sm: 'calc(var(--radius) - 4px)'
68 | }
69 | }
70 | },
71 | plugins: [require("tailwindcss-animate")],
72 | } satisfies Config;
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------