├── .eslintrc.json ├── bun.lockb ├── public ├── og.png ├── og-old.png ├── vercel.svg ├── next.svg └── llms.txt ├── app ├── favicon.ico ├── (components) │ ├── example-variants.ts │ └── example-code.tsx ├── layout.tsx ├── globals.css └── page.tsx ├── next.config.mjs ├── postcss.config.mjs ├── components.json ├── .gitignore ├── config └── site.ts ├── tsconfig.json ├── lib └── utils.ts ├── components ├── ui │ ├── sonner.tsx │ ├── button.tsx │ ├── calendar-heatmap.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── code.tsx ├── site-footer.tsx ├── site-header.tsx ├── page-header.tsx ├── copy-button.tsx └── icons.tsx ├── LICENSE ├── package.json ├── tailwind.config.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gurbaaz27/shadcn-calendar-heatmap/HEAD/bun.lockb -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gurbaaz27/shadcn-calendar-heatmap/HEAD/public/og.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gurbaaz27/shadcn-calendar-heatmap/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/og-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gurbaaz27/shadcn-calendar-heatmap/HEAD/public/og-old.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | title: "Shadcn Calendar Heatmap", 3 | name: "gurbaaz27/shadcn-calendar-heatmap", 4 | url: "https://shadcn-calendar-heatmap.vercel.app", 5 | ogImage: "https://shadcn-calendar-heatmap.vercel.app/og.png", 6 | description: 7 | "Accessible. Unstyled. Customizable. Open Source. Build your own calendar heatmap effortlessly.", 8 | links: { 9 | twitter: "https://x.com/GurbaazNandra", 10 | github: "https://github.com/gurbaaz27/shadcn-calendar-heatmap", 11 | }, 12 | } 13 | export type SiteConfig = typeof siteConfig 14 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export function randomDate(start: Date, end: Date) { 9 | return new Date( 10 | start.getTime() + Math.random() * (end.getTime() - start.getTime()) 11 | ) 12 | } 13 | 14 | export function currentMonthFirstDate() { 15 | const date = new Date() 16 | return new Date(date.getFullYear(), date.getMonth(), 1) 17 | } 18 | 19 | export function currentMonthLastDate(month: number = 1) { 20 | const date = new Date() 21 | return new Date(date.getFullYear(), date.getMonth() + month, 0) 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gurbaaz Singh Nandra 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 | -------------------------------------------------------------------------------- /components/code.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { unified } from "unified" 3 | import remarkParse from "remark-parse" 4 | import remarkRehype from "remark-rehype" 5 | import rehypeStringify from "rehype-stringify" 6 | import rehypePrettyCode from "rehype-pretty-code" 7 | import { CopyButton } from "./copy-button" 8 | 9 | export async function Code({ 10 | code, 11 | toCopy, 12 | dark = true, 13 | }: { 14 | code: string 15 | toCopy?: string 16 | dark?: boolean 17 | }) { 18 | const highlightedCode = await highlightCode(code, dark) 19 | return ( 20 |
21 |
26 | 
27 |       {toCopy && (
28 |         
29 | 30 |
31 | )} 32 |
33 | ) 34 | } 35 | 36 | async function highlightCode(code: string, dark: boolean) { 37 | const file = await unified() 38 | .use(remarkParse) 39 | .use(remarkRehype) 40 | .use(rehypePrettyCode, { 41 | keepBackground: false, 42 | theme: dark ? "vesper" : "github-light", 43 | }) 44 | .use(rehypeStringify) 45 | .process(code) 46 | 47 | return String(file) 48 | } 49 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "../config/site" 2 | 3 | export function SiteFooter() { 4 | return ( 5 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadcn-calendar-heatmap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dropdown-menu": "^2.1.1", 13 | "@radix-ui/react-icons": "^1.3.0", 14 | "@radix-ui/react-select": "^2.1.1", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.1", 18 | "date-fns": "^3.6.0", 19 | "geist": "^1.3.0", 20 | "lucide-react": "^0.396.0", 21 | "next": "14.2.4", 22 | "next-themes": "^0.3.0", 23 | "react": "^18", 24 | "react-day-picker": "^8.10.1", 25 | "react-dom": "^18", 26 | "rehype-pretty-code": "^0.13.2", 27 | "rehype-stringify": "^10.0.0", 28 | "remark-parse": "^11.0.0", 29 | "remark-rehype": "^11.1.0", 30 | "sonner": "^1.5.0", 31 | "tailwind-merge": "^2.3.0", 32 | "tailwindcss-animate": "^1.0.7", 33 | "unified": "^11.0.5" 34 | }, 35 | "devDependencies": { 36 | "typescript": "^5", 37 | "@types/node": "^20", 38 | "@types/react": "^18", 39 | "@types/react-dom": "^18", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "eslint": "^8", 43 | "eslint-config-next": "14.2.4" 44 | }, 45 | "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" 46 | } 47 | -------------------------------------------------------------------------------- /components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { siteConfig } from "../config/site" 4 | import { cn } from "../lib/utils" 5 | import { buttonVariants } from "./ui/button" 6 | import { Icons } from "./icons" 7 | 8 | export function SiteHeader() { 9 | return ( 10 |
11 |
12 |
13 | 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /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 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", 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 | -------------------------------------------------------------------------------- /components/page-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "../lib/utils" 2 | 3 | function PageHeader({ 4 | className, 5 | children, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
16 | {children} 17 |
18 | ) 19 | } 20 | 21 | function PageHeaderNotifier({ 22 | className, 23 | ...props 24 | }: React.HTMLAttributes) { 25 | return ( 26 |

36 | ) 37 | } 38 | 39 | function PageHeaderHeading({ 40 | className, 41 | ...props 42 | }: React.HTMLAttributes) { 43 | return ( 44 |

51 | ) 52 | } 53 | 54 | function PageHeaderDescription({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) { 58 | return ( 59 |

66 | ) 67 | } 68 | 69 | function PageActions({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) { 73 | return ( 74 |

81 | ) 82 | } 83 | 84 | export { 85 | PageHeader, 86 | PageHeaderNotifier, 87 | PageHeaderHeading, 88 | PageHeaderDescription, 89 | PageActions, 90 | } 91 | -------------------------------------------------------------------------------- /app/(components)/example-variants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | currentMonthFirstDate, 3 | currentMonthLastDate, 4 | randomDate, 5 | } from "@/lib/utils" 6 | 7 | export const GithubStreak = [ 8 | "text-white hover:text-white bg-green-400 hover:bg-green-400", 9 | "text-white hover:text-white bg-green-500 hover:bg-green-500", 10 | "text-white hover:text-white bg-green-700 hover:bg-green-700", 11 | ] 12 | 13 | export const GithubStreakDates = [ 14 | [...Array(12)].map((_) => 15 | randomDate(currentMonthFirstDate(), currentMonthLastDate(3)) 16 | ), 17 | [...Array(9)].map((_) => 18 | randomDate(currentMonthFirstDate(), currentMonthLastDate(3)) 19 | ), 20 | [...Array(6)].map((_) => 21 | randomDate(currentMonthFirstDate(), currentMonthLastDate(3)) 22 | ), 23 | ] 24 | 25 | export const Heatmap = [ 26 | "text-white hover:text-white bg-blue-300 hover:bg-blue-300", 27 | "text-white hover:text-white bg-green-500 hover:bg-green-500", 28 | "text-white hover:text-white bg-amber-400 hover:bg-amber-400", 29 | "text-white hover:text-white bg-red-700 hover:bg-red-700", 30 | ] 31 | 32 | export const HeatmapDatesWeight = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 33 | 34 | export const Rainbow = [ 35 | "text-white hover:text-white bg-violet-400 hover:bg-violet-400", 36 | "text-white hover:text-white bg-indigo-400 hover:bg-indigo-400", 37 | "text-white hover:text-white bg-blue-400 hover:bg-blue-400", 38 | "text-white hover:text-white bg-green-400 hover:bg-green-400", 39 | "text-white hover:text-white bg-yellow-400 hover:bg-yellow-400", 40 | "text-white hover:text-white bg-orange-400 hover:bg-orange-400", 41 | "text-white hover:text-white bg-red-400 hover:bg-red-400", 42 | ] 43 | 44 | export const RainbowDates = [ 45 | [...Array(3)].map((_) => 46 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 47 | ), 48 | [...Array(2)].map((_) => 49 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 50 | ), 51 | [...Array(1)].map((_) => 52 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 53 | ), 54 | [...Array(3)].map((_) => 55 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 56 | ), 57 | [...Array(2)].map((_) => 58 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 59 | ), 60 | [...Array(1)].map((_) => 61 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 62 | ), 63 | [...Array(3)].map((_) => 64 | randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css" 2 | import type { Metadata } from "next" 3 | import { cn } from "@/lib/utils" 4 | import { GeistSans } from "geist/font/sans" 5 | import { JetBrains_Mono } from "next/font/google" 6 | import { SiteHeader } from "@/components/site-header" 7 | import { SiteFooter } from "@/components/site-footer" 8 | import { Toaster } from "@/components/ui/sonner" 9 | import { siteConfig } from "@/config/site" 10 | 11 | export const fontMono = JetBrains_Mono({ 12 | subsets: ["latin"], 13 | variable: "--font-mono", 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: { 18 | default: siteConfig.title, 19 | template: `%s - ${siteConfig.name}`, 20 | }, 21 | metadataBase: new URL(siteConfig.url), 22 | description: siteConfig.description, 23 | keywords: [ 24 | "React", 25 | "Heatmap", 26 | "Calendar", 27 | "Next.js", 28 | "Tailwind CSS", 29 | "Server Components", 30 | "Accessible", 31 | "Shadcn", 32 | ], 33 | authors: [ 34 | { 35 | name: "gurbaaz", 36 | url: "https://gurbaaz.me", 37 | }, 38 | ], 39 | creator: "gurbaaz", 40 | openGraph: { 41 | type: "website", 42 | locale: "en_IN", 43 | url: siteConfig.url, 44 | title: siteConfig.name, 45 | description: siteConfig.description, 46 | siteName: siteConfig.name, 47 | images: [ 48 | { 49 | url: siteConfig.ogImage, 50 | width: 1200, 51 | height: 630, 52 | alt: siteConfig.name, 53 | }, 54 | ], 55 | }, 56 | twitter: { 57 | card: "summary_large_image", 58 | title: siteConfig.name, 59 | description: siteConfig.description, 60 | images: [siteConfig.ogImage], 61 | creator: "@GurbaazNandra", 62 | }, 63 | icons: { 64 | icon: "/favicon.ico", 65 | shortcut: "/favicon-16x16.png", 66 | apple: "/apple-touch-icon.png", 67 | }, 68 | } 69 | 70 | export default function RootLayout({ 71 | children, 72 | }: Readonly<{ 73 | children: React.ReactNode 74 | }>) { 75 | return ( 76 | 77 | 83 |
84 | 85 |
{children}
86 | 87 |
88 | 89 | 90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /app/(components)/example-code.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@/components/code" 2 | 3 | const tsx = `import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" 4 | 5 | // Github-style streak pattern 6 | 18 | 19 | // Or you may simply pass weighted array of dates, 20 | // and they would be slotted to different variants based on length of \`variantClassnames\` 21 | 33 | 34 | // Component code at https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx 35 | ` 36 | 37 | const code = `\`\`\`tsx /maxLength={6}/ /render/ /slots/1 /.map((slot, idx)/1 /Slot/2,3,4 /props.char/2 // 38 | ${tsx} 39 | \`\`\`` 40 | 41 | export function ExampleCode() { 42 | return ( 43 |
44 |
45 | 46 | 47 |
48 | 49 |
50 | 51 | {/* Anchor */} 52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{ts,tsx}", 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | "fade-in": { 71 | from: { 72 | opacity: "0", 73 | }, 74 | to: { opacity: "1" }, 75 | }, 76 | "fade-up": { 77 | from: { 78 | opacity: "0", 79 | transform: "translateY(var(--fade-distance, .25rem))", 80 | }, 81 | to: { opacity: "1", transform: "translateY(0)" }, 82 | }, 83 | }, 84 | animation: { 85 | "accordion-down": "accordion-down 0.2s ease-out", 86 | "accordion-up": "accordion-up 0.2s ease-out", 87 | "fade-in": "fade-in 0.3s ease-out forwards", 88 | "fade-up": "fade-up 1s ease-out forwards", 89 | }, 90 | }, 91 | }, 92 | plugins: [require("tailwindcss-animate")], 93 | } satisfies Config 94 | 95 | export default config 96 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 72.22% 50.59%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5% 64.9%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @media (prefers-color-scheme: dark) { 53 | :root { 54 | --background: 240 10% 3.9%; 55 | --foreground: 0 0% 98%; 56 | --card: 240 10% 3.9%; 57 | --card-foreground: 0 0% 98%; 58 | --popover: 240 10% 3.9%; 59 | --popover-foreground: 0 0% 98%; 60 | --primary: 0 0% 98%; 61 | --primary-foreground: 240 5.9% 10%; 62 | --secondary: 240 3.7% 15.9%; 63 | --secondary-foreground: 0 0% 98%; 64 | --muted: 240 3.7% 15.9%; 65 | --muted-foreground: 240 5% 64.9%; 66 | --accent: 240 3.7% 15.9%; 67 | --accent-foreground: 0 0% 98%; 68 | --destructive: 0 62.8% 30.6%; 69 | --destructive-foreground: 0 85.7% 97.3%; 70 | --border: 240 3.7% 15.9%; 71 | --input: 240 3.7% 15.9%; 72 | --ring: 240 4.9% 83.9%; 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | /* @apply bg-background text-foreground selection:bg-[#6B2BF4] selection:text-foreground; */ 82 | @apply bg-background text-foreground; 83 | /* font-feature-settings: "rlig" 1, "calt" 1; */ 84 | font-synthesis-weight: none; 85 | text-rendering: optimizeLegibility; 86 | } 87 | } 88 | 89 | @layer utilities { 90 | } 91 | 92 | [data-highlighted-chars] { 93 | @apply bg-zinc-900 rounded; 94 | box-shadow: 2px 2px 0 2px rgba(139, 139, 148, 0.5); 95 | } 96 | [data-highlighted-chars] .dark { 97 | @apply bg-zinc-700/50 rounded; 98 | box-shadow: 2px 2px 0 2px rgba(139, 139, 148, 0.5); 99 | } 100 | [data-highlighted-chars] * { 101 | @apply !text-white; 102 | } 103 | [data-rehype-pretty-code-figure] pre { 104 | @apply pb-4 pt-6 max-h-[650px] overflow-x-auto rounded-lg border !bg-transparent; 105 | } 106 | [data-rehype-pretty-code-figure] [data-line] { 107 | @apply inline-block min-h-4 w-full py-0.5 px-4; 108 | } 109 | 110 | .code-example-overlay { 111 | background-image: linear-gradient( 112 | to bottom, 113 | theme("colors.background") 60%, 114 | transparent 115 | ); 116 | transform: translateY(0); 117 | animation: move-overlay 4s ease-out forwards; 118 | animation-delay: 3s; 119 | } 120 | .code-example-light { 121 | } 122 | .code-example-dark { 123 | display: none; 124 | } 125 | @media (prefers-color-scheme: dark) { 126 | .code-example-light { 127 | display: none; 128 | } 129 | .code-example-dark { 130 | display: unset; 131 | } 132 | } 133 | 134 | @media (prefers-reduced-motion: reduce) { 135 | .code-example-overlay { 136 | opacity: 0; 137 | animation: none; 138 | } 139 | } 140 | @keyframes move-overlay { 141 | 0% { 142 | transform: translateY(0); 143 | } 144 | 100% { 145 | transform: translateY(-100%); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | // Stolen from @shadcn/ui the man the machine!! 2 | "use client" 3 | 4 | import * as React from "react" 5 | import { CheckIcon, CopyIcon } from "@radix-ui/react-icons" 6 | import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Button } from "./ui/button" 10 | import { 11 | DropdownMenu, 12 | DropdownMenuContent, 13 | DropdownMenuItem, 14 | DropdownMenuTrigger, 15 | } from "./ui/dropdown-menu" 16 | 17 | interface CopyButtonProps extends React.HTMLAttributes { 18 | value: string 19 | src?: string 20 | } 21 | 22 | export async function copyToClipboardWithMeta(value: string) { 23 | window && window.isSecureContext && navigator.clipboard.writeText(value) 24 | } 25 | 26 | export function CopyButton({ 27 | value, 28 | className, 29 | src, 30 | ...props 31 | }: CopyButtonProps) { 32 | const [hasCopied, setHasCopied] = React.useState(false) 33 | 34 | React.useEffect(() => { 35 | setTimeout(() => { 36 | setHasCopied(false) 37 | }, 2000) 38 | }, [hasCopied]) 39 | 40 | return ( 41 | 61 | ) 62 | } 63 | 64 | interface CopyWithClassNamesProps extends DropdownMenuTriggerProps { 65 | value: string 66 | classNames: string 67 | className?: string 68 | } 69 | 70 | export function CopyWithClassNames({ 71 | value, 72 | classNames, 73 | className, 74 | ...props 75 | }: CopyWithClassNamesProps) { 76 | const [hasCopied, setHasCopied] = React.useState(false) 77 | 78 | React.useEffect(() => { 79 | setTimeout(() => { 80 | setHasCopied(false) 81 | }, 2000) 82 | }, [hasCopied]) 83 | 84 | const copyToClipboard = React.useCallback((value: string) => { 85 | copyToClipboardWithMeta(value) 86 | setHasCopied(true) 87 | }, []) 88 | 89 | return ( 90 | 91 | 92 | 107 | 108 | 109 | copyToClipboard(value)}> 110 | Component 111 | 112 | copyToClipboard(classNames)}> 113 | Classname 114 | 115 | 116 | 117 | ) 118 | } 119 | 120 | interface CopyNpmCommandButtonProps extends DropdownMenuTriggerProps { 121 | commands: { 122 | __npmCommand__: string 123 | __yarnCommand__: string 124 | __pnpmCommand__: string 125 | __bunCommand__: string 126 | } 127 | } 128 | 129 | export function CopyNpmCommandButton({ 130 | commands, 131 | className, 132 | ...props 133 | }: CopyNpmCommandButtonProps) { 134 | const [hasCopied, setHasCopied] = React.useState(false) 135 | 136 | React.useEffect(() => { 137 | setTimeout(() => { 138 | setHasCopied(false) 139 | }, 2000) 140 | }, [hasCopied]) 141 | 142 | const copyCommand = React.useCallback( 143 | (value: string, pm: "npm" | "pnpm" | "yarn" | "bun") => { 144 | copyToClipboardWithMeta(value) 145 | setHasCopied(true) 146 | }, 147 | [] 148 | ) 149 | 150 | return ( 151 | 152 | 153 | 168 | 169 | 170 | copyCommand(commands.__npmCommand__, "npm")} 172 | > 173 | npm 174 | 175 | copyCommand(commands.__yarnCommand__, "yarn")} 177 | > 178 | yarn 179 | 180 | copyCommand(commands.__pnpmCommand__, "pnpm")} 182 | > 183 | pnpm 184 | 185 | copyCommand(commands.__bunCommand__, "bun")} 187 | > 188 | bun 189 | 190 | 191 | 192 | ) 193 | } 194 | -------------------------------------------------------------------------------- /components/ui/calendar-heatmap.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | // type utilities 11 | type UnionKeys = T extends T ? keyof T : never 12 | type Expand = T extends T ? { [K in keyof T]: T[K] } : never 13 | type OneOf = { 14 | [K in keyof T]: Expand< 15 | T[K] & Partial, keyof T[K]>, never>> 16 | > 17 | }[number] 18 | 19 | // types 20 | export type Classname = string 21 | export type WeightedDateEntry = { 22 | date: Date 23 | weight: number 24 | } 25 | 26 | interface IDatesPerVariant { 27 | datesPerVariant: Date[][] 28 | } 29 | interface IWeightedDatesEntry { 30 | weightedDates: WeightedDateEntry[] 31 | } 32 | 33 | type VariantDatesInput = OneOf<[IDatesPerVariant, IWeightedDatesEntry]> 34 | 35 | export type CalendarProps = React.ComponentProps & { 36 | variantClassnames: Classname[] 37 | } & VariantDatesInput 38 | 39 | /// utlity functions 40 | function useModifers( 41 | variantClassnames: Classname[], 42 | datesPerVariant: Date[][] 43 | ): [Record, Record] { 44 | const noOfVariants = variantClassnames.length 45 | 46 | const variantLabels = [...Array(noOfVariants)].map( 47 | (_, idx) => `__variant${idx}` 48 | ) 49 | 50 | const modifiers = variantLabels.reduce((acc, key, index) => { 51 | acc[key] = datesPerVariant[index] 52 | return acc 53 | }, {} as Record) 54 | 55 | const modifiersClassNames = variantLabels.reduce((acc, key, index) => { 56 | acc[key] = variantClassnames[index] 57 | return acc 58 | }, {} as Record) 59 | 60 | return [modifiers, modifiersClassNames] 61 | } 62 | 63 | function categorizeDatesPerVariant( 64 | weightedDates: WeightedDateEntry[], 65 | noOfVariants: number 66 | ) { 67 | const sortedEntries = weightedDates.sort((a, b) => a.weight - b.weight) 68 | 69 | const categorizedRecord = [...Array(noOfVariants)].map(() => [] as Date[]) 70 | 71 | const minNumber = sortedEntries[0].weight 72 | const maxNumber = sortedEntries[sortedEntries.length - 1].weight 73 | const range = minNumber == maxNumber ? 1 : (maxNumber - minNumber) / noOfVariants; 74 | 75 | sortedEntries.forEach((entry) => { 76 | const category = Math.min( 77 | Math.floor((entry.weight - minNumber) / range), 78 | noOfVariants - 1 79 | ) 80 | categorizedRecord[category].push(entry.date) 81 | }) 82 | 83 | return categorizedRecord 84 | } 85 | 86 | function CalendarHeatmap({ 87 | variantClassnames, 88 | datesPerVariant, 89 | weightedDates, 90 | className, 91 | classNames, 92 | showOutsideDays = true, 93 | ...props 94 | }: CalendarProps) { 95 | const noOfVariants = variantClassnames.length 96 | 97 | weightedDates = weightedDates ?? [] 98 | datesPerVariant = 99 | datesPerVariant ?? categorizeDatesPerVariant(weightedDates, noOfVariants) 100 | 101 | const [modifiers, modifiersClassNames] = useModifers( 102 | variantClassnames, 103 | datesPerVariant 104 | ) 105 | 106 | return ( 107 | , 148 | IconRight: ({ ...props }) => , 149 | }} 150 | {...props} 151 | /> 152 | ) 153 | } 154 | CalendarHeatmap.displayName = "CalendarHeatmap" 155 | 156 | export { CalendarHeatmap } 157 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | PageActions, 3 | PageHeader, 4 | PageHeaderDescription, 5 | PageHeaderHeading, 6 | PageHeaderNotifier, 7 | } from "@/components/page-header" 8 | import { buttonVariants } from "@/components/ui/button" 9 | import { siteConfig } from "@/config/site" 10 | import { 11 | cn, 12 | currentMonthFirstDate, 13 | currentMonthLastDate, 14 | randomDate, 15 | } from "@/lib/utils" 16 | import Link from "next/link" 17 | import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" 18 | import { Icons } from "@/components/icons" 19 | import { Star } from "lucide-react" 20 | import { ExampleCode } from "./(components)/example-code" 21 | import { 22 | GithubStreak, 23 | GithubStreakDates, 24 | Heatmap, 25 | HeatmapDatesWeight, 26 | Rainbow, 27 | RainbowDates, 28 | } from "./(components)/example-variants" 29 | 30 | const fadeUpClassname = 31 | "lg:motion-safe:opacity-0 lg:motion-safe:animate-fade-up" 32 | 33 | async function getRepoStarCount() { 34 | const res = await fetch(`https://api.github.com/repos/${siteConfig.name}`) 35 | const data = await res.json() 36 | const starCount = data.stargazers_count 37 | 38 | if (starCount > 999) { 39 | return (starCount / 1000).toFixed(1) + "K" 40 | } 41 | 42 | return starCount 43 | } 44 | 45 | export default async function IndexPage() { 46 | const starCount = await getRepoStarCount() 47 | 48 | return ( 49 |
50 | 51 | 52 | Excited to officially launch our new shadcn-based component! 53 | 🎉 54 | 55 | 56 | 57 | Modern alternative to primitive react heatmaps. 58 | 59 | 60 | 68 | 69 | 75 | Showcase Github streaks. Visualise user growth. Understand global 76 | warming trends.

77 | Convey more with less. 78 |

79 | Unstyled. Customizable. Open Source. 80 |
81 | 82 | 88 | 97 | 98 |
99 |
{siteConfig.name}
100 |
101 | 102 |
{starCount}
103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 119 | Examples 120 | 121 | 122 | 129 | Github Streaks 130 | 131 | 132 | 141 | 142 | 149 | Temperature Heatmap 150 | 151 | 152 | ({ 159 | date: randomDate(currentMonthFirstDate(), currentMonthLastDate()), 160 | weight: wgt, 161 | }))} 162 | /> 163 | 164 | 171 | Rainbow Colors 172 | 173 | 174 | 183 | 184 |
185 | ) 186 | } 187 | 188 | export const revalidate = 3600 189 | -------------------------------------------------------------------------------- /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, 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shadcn-calendar-heatmap 2 | 3 | A modern, customizable calendar heatmap component built on top of [react-day-picker](https://react-day-picker.js.org/) following [shadcn/ui](https://ui.shadcn.com/) patterns. 4 | 5 | **Accessible. Unstyled. Customizable. Open Source.** 6 | 7 | ![og](public/og.png) 8 | 9 | ## ✨ Features 10 | 11 | - 🎨 **Fully Customizable** - Style with Tailwind CSS classes 12 | - 📅 **Multiple Data Modes** - Use direct date arrays or weighted dates with auto-categorization 13 | - 🔢 **Multi-month Support** - Display any number of months 14 | - ♿ **Accessible** - Built on react-day-picker with full keyboard navigation 15 | - 🎯 **Type Safe** - Written in TypeScript with full type definitions 16 | - 🌈 **Preset Variants** - GitHub streaks, temperature heatmaps, rainbow colors, and more 17 | 18 | ## 🚀 Demo 19 | 20 | Check out the live demo at [shadcn-calendar-heatmap.vercel.app](https://shadcn-calendar-heatmap.vercel.app) 21 | 22 | ## 📦 Installation 23 | 24 | This component follows the shadcn/ui philosophy - copy the component directly into your project. 25 | 26 | ### 1. Install Dependencies 27 | 28 | ```bash 29 | npm install react-day-picker date-fns lucide-react 30 | # or 31 | yarn add react-day-picker date-fns lucide-react 32 | # or 33 | pnpm add react-day-picker date-fns lucide-react 34 | ``` 35 | 36 | ### 2. Copy the Component 37 | 38 | Copy [`components/ui/calendar-heatmap.tsx`](https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx) into your project's components directory. 39 | 40 | ### 3. Ensure you have the required utilities 41 | 42 | Make sure you have the `cn` utility function (standard in shadcn/ui projects): 43 | 44 | ```typescript 45 | // lib/utils.ts 46 | import { type ClassValue, clsx } from "clsx" 47 | import { twMerge } from "tailwind-merge" 48 | 49 | export function cn(...inputs: ClassValue[]) { 50 | return twMerge(clsx(inputs)) 51 | } 52 | ``` 53 | 54 | ## 📖 Usage 55 | 56 | ### Basic Example - GitHub Contribution Graph 57 | 58 | ```tsx 59 | import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" 60 | 61 | export default function MyComponent() { 62 | return ( 63 | 75 | ) 76 | } 77 | ``` 78 | 79 | ### Using Weighted Dates 80 | 81 | Pass dates with numeric weights, and the component auto-categorizes them: 82 | 83 | ```tsx 84 | 97 | ``` 98 | 99 | ### Multi-month Display 100 | 101 | ```tsx 102 | 107 | ``` 108 | 109 | ## 🔧 API Reference 110 | 111 | ### Props 112 | 113 | | Prop | Type | Required | Description | 114 | |------|------|----------|-------------| 115 | | `variantClassnames` | `string[]` | ✅ | Array of Tailwind CSS classes for each intensity level | 116 | | `datesPerVariant` | `Date[][]` | ⚡ | 2D array where each inner array contains dates for that variant | 117 | | `weightedDates` | `WeightedDateEntry[]` | ⚡ | Array of `{ date: Date, weight: number }` objects | 118 | | `numberOfMonths` | `number` | ❌ | Number of months to display (default: 1) | 119 | | `showOutsideDays` | `boolean` | ❌ | Show days from adjacent months (default: true) | 120 | 121 | > ⚡ You must provide either `datesPerVariant` OR `weightedDates`, not both. 122 | 123 | The component also accepts all props from [react-day-picker](https://react-day-picker.js.org/api/interfaces/DayPickerMultipleProps). 124 | 125 | ### Types 126 | 127 | ```typescript 128 | type WeightedDateEntry = { 129 | date: Date 130 | weight: number 131 | } 132 | ``` 133 | 134 | ## 🎨 Customization Examples 135 | 136 | ### Temperature Heatmap 137 | 138 | ```tsx 139 | const Heatmap = [ 140 | "text-white hover:text-white bg-blue-300 hover:bg-blue-300", // Cold 141 | "text-white hover:text-white bg-green-500 hover:bg-green-500", // Mild 142 | "text-white hover:text-white bg-amber-400 hover:bg-amber-400", // Warm 143 | "text-white hover:text-white bg-red-700 hover:bg-red-700", // Hot 144 | ] 145 | ``` 146 | 147 | ### Rainbow Colors 148 | 149 | ```tsx 150 | const Rainbow = [ 151 | "text-white hover:text-white bg-violet-400 hover:bg-violet-400", 152 | "text-white hover:text-white bg-indigo-400 hover:bg-indigo-400", 153 | "text-white hover:text-white bg-blue-400 hover:bg-blue-400", 154 | "text-white hover:text-white bg-green-400 hover:bg-green-400", 155 | "text-white hover:text-white bg-yellow-400 hover:bg-yellow-400", 156 | "text-white hover:text-white bg-orange-400 hover:bg-orange-400", 157 | "text-white hover:text-white bg-red-400 hover:bg-red-400", 158 | ] 159 | ``` 160 | 161 | ## ⭐ Star History 162 | 163 | 164 | 165 | 166 | 167 | Star History Chart 168 | 169 | 170 | 171 | ## 🤝 Contributing 172 | 173 | Contributions are welcome! Feel free to open an issue or submit a pull request. 174 | 175 | ## 📄 License 176 | 177 | MIT © [Gurbaaz Singh Nandra](https://x.com/GurbaazNandra) 178 | 179 | ## 🔗 Links 180 | 181 | - [Live Demo](https://shadcn-calendar-heatmap.vercel.app) 182 | - [GitHub Repository](https://github.com/gurbaaz27/shadcn-calendar-heatmap) 183 | - [Twitter/X](https://x.com/GurbaazNandra) 184 | -------------------------------------------------------------------------------- /public/llms.txt: -------------------------------------------------------------------------------- 1 | # shadcn-calendar-heatmap 2 | 3 | > A customizable calendar heatmap component built on top of react-day-picker, following shadcn/ui patterns. Accessible, unstyled by default, and fully customizable with Tailwind CSS. 4 | 5 | ## Overview 6 | 7 | shadcn-calendar-heatmap is a React component that transforms the DayPicker calendar into a heatmap visualization. It allows you to display dates with varying intensities using custom color variants - perfect for GitHub contribution graphs, temperature heatmaps, activity tracking, and more. 8 | 9 | ## Installation 10 | 11 | The component is designed to be copied directly into your project following the shadcn/ui philosophy. Copy the component from: 12 | https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx 13 | 14 | ### Dependencies 15 | 16 | - react-day-picker (^8.10.1) 17 | - tailwind-merge 18 | - class-variance-authority 19 | - lucide-react (for icons) 20 | 21 | ## Component API 22 | 23 | ### CalendarHeatmap Props 24 | 25 | The component extends all props from `react-day-picker`'s DayPicker, plus: 26 | 27 | | Prop | Type | Required | Description | 28 | |------|------|----------|-------------| 29 | | `variantClassnames` | `string[]` | Yes | Array of Tailwind CSS classes for each intensity level | 30 | | `datesPerVariant` | `Date[][]` | One of these | 2D array where each inner array contains dates for that variant | 31 | | `weightedDates` | `WeightedDateEntry[]` | One of these | Array of `{ date: Date, weight: number }` objects | 32 | | `numberOfMonths` | `number` | No | Number of months to display (default: 1) | 33 | | `showOutsideDays` | `boolean` | No | Show days from adjacent months (default: true) | 34 | 35 | **Note:** You must provide either `datesPerVariant` OR `weightedDates`, not both. 36 | 37 | ### WeightedDateEntry Type 38 | 39 | ```typescript 40 | type WeightedDateEntry = { 41 | date: Date 42 | weight: number 43 | } 44 | ``` 45 | 46 | ## Usage Examples 47 | 48 | ### Basic GitHub-style Contribution Graph 49 | 50 | ```tsx 51 | import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" 52 | 53 | 65 | ``` 66 | 67 | ### Using Weighted Dates (Auto-categorization) 68 | 69 | When you have numeric data associated with dates, use `weightedDates`. The component automatically categorizes dates into variants based on their weights: 70 | 71 | ```tsx 72 | 88 | ``` 89 | 90 | ### Multi-month Display 91 | 92 | ```tsx 93 | 106 | ``` 107 | 108 | ### Rainbow Variant Example 109 | 110 | ```tsx 111 | const Rainbow = [ 112 | "text-white hover:text-white bg-violet-400 hover:bg-violet-400", 113 | "text-white hover:text-white bg-indigo-400 hover:bg-indigo-400", 114 | "text-white hover:text-white bg-blue-400 hover:bg-blue-400", 115 | "text-white hover:text-white bg-green-400 hover:bg-green-400", 116 | "text-white hover:text-white bg-yellow-400 hover:bg-yellow-400", 117 | "text-white hover:text-white bg-orange-400 hover:bg-orange-400", 118 | "text-white hover:text-white bg-red-400 hover:bg-red-400", 119 | ] 120 | 121 | 126 | ``` 127 | 128 | ## How It Works 129 | 130 | 1. **Variant Classes**: Each string in `variantClassnames` represents a color/style intensity level. The component creates DayPicker modifiers for each variant. 131 | 132 | 2. **Date Mapping**: 133 | - With `datesPerVariant`: Dates in the first array get the first variant class, second array gets second class, etc. 134 | - With `weightedDates`: The component sorts dates by weight, divides the range into equal segments based on the number of variants, and assigns each date to its corresponding variant. 135 | 136 | 3. **Styling**: The component uses Tailwind CSS classes. Include both normal and hover states in your variant classes for consistent interaction feedback. 137 | 138 | ## Customization 139 | 140 | ### Custom Styling 141 | 142 | Override default calendar styles via the `classNames` prop: 143 | 144 | ```tsx 145 | 155 | ``` 156 | 157 | ### Additional Props 158 | 159 | Since the component extends DayPicker, you can use any DayPicker prop: 160 | 161 | ```tsx 162 | console.log(day)} 169 | /> 170 | ``` 171 | 172 | ## Links 173 | 174 | - Demo: https://shadcn-calendar-heatmap.vercel.app 175 | - GitHub: https://github.com/gurbaaz27/shadcn-calendar-heatmap 176 | - Component Source: https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx 177 | 178 | ## License 179 | 180 | MIT 181 | 182 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | type IconProps = React.HTMLAttributes 2 | 3 | export const Icons = { 4 | logo: (props: IconProps) => ( 5 | 6 | 7 | 18 | 29 | 30 | ), 31 | twitter: (props: IconProps) => ( 32 | 39 | 40 | 41 | ), 42 | gitHub: (props: IconProps) => ( 43 | 44 | 48 | 49 | ), 50 | radix: (props: IconProps) => ( 51 | 52 | 56 | 57 | 61 | 62 | ), 63 | aria: (props: IconProps) => ( 64 | 65 | 66 | 67 | ), 68 | npm: (props: IconProps) => ( 69 | 70 | 74 | 75 | ), 76 | yarn: (props: IconProps) => ( 77 | 78 | 82 | 83 | ), 84 | pnpm: (props: IconProps) => ( 85 | 86 | 90 | 91 | ), 92 | react: (props: IconProps) => ( 93 | 94 | 98 | 99 | ), 100 | tailwind: (props: IconProps) => ( 101 | 102 | 106 | 107 | ), 108 | google: (props: IconProps) => ( 109 | 110 | 114 | 115 | ), 116 | apple: (props: IconProps) => ( 117 | 118 | 122 | 123 | ), 124 | paypal: (props: IconProps) => ( 125 | 126 | 130 | 131 | ), 132 | spinner: (props: IconProps) => ( 133 | 145 | 146 | 147 | ), 148 | } 149 | --------------------------------------------------------------------------------