Repository: gurbaaz27/shadcn-calendar-heatmap Branch: main Commit: 759856717f14 Files: 31 Total size: 78.0 KB Directory structure: gitextract_l_cd6g81/ ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── (components)/ │ │ ├── copy-llms-button.tsx │ │ ├── example-code.tsx │ │ └── example-variants.ts │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── bun.lockb ├── components/ │ ├── code.tsx │ ├── copy-button.tsx │ ├── icons.tsx │ ├── page-header.tsx │ ├── site-footer.tsx │ ├── site-header.tsx │ └── ui/ │ ├── button.tsx │ ├── calendar-heatmap.tsx │ ├── dropdown-menu.tsx │ ├── select.tsx │ └── sonner.tsx ├── components.json ├── config/ │ └── site.ts ├── lib/ │ └── utils.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public/ │ └── llms.txt ├── tailwind.config.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Gurbaaz Singh Nandra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # shadcn-calendar-heatmap 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. **Accessible. Unstyled. Customizable. Open Source.** ![og](public/og.png) ## ✨ Features - 🎨 **Fully Customizable** - Style with Tailwind CSS classes - 📅 **Multiple Data Modes** - Use direct date arrays or weighted dates with auto-categorization - 🔢 **Multi-month Support** - Display any number of months - ♿ **Accessible** - Built on react-day-picker with full keyboard navigation - 🎯 **Type Safe** - Written in TypeScript with full type definitions - 🌈 **Preset Variants** - GitHub streaks, temperature heatmaps, rainbow colors, and more ## 🚀 Demo Check out the live demo at [shadcn-calendar-heatmap.vercel.app](https://shadcn-calendar-heatmap.vercel.app) ## 📦 Installation This component follows the shadcn/ui philosophy - copy the component directly into your project. ### 1. Install Dependencies ```bash npm install react-day-picker date-fns lucide-react # or yarn add react-day-picker date-fns lucide-react # or pnpm add react-day-picker date-fns lucide-react ``` ### 2. Copy the Component 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. ### 3. Ensure you have the required utilities Make sure you have the `cn` utility function (standard in shadcn/ui projects): ```typescript // lib/utils.ts import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } ``` ## 📖 Usage ### Basic Example - GitHub Contribution Graph ```tsx import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" export default function MyComponent() { return ( ) } ``` ### Using Weighted Dates Pass dates with numeric weights, and the component auto-categorizes them: ```tsx ``` ### Multi-month Display ```tsx ``` ## 🔧 API Reference ### Props | Prop | Type | Required | Description | |------|------|----------|-------------| | `variantClassnames` | `string[]` | ✅ | Array of Tailwind CSS classes for each intensity level | | `datesPerVariant` | `Date[][]` | ⚡ | 2D array where each inner array contains dates for that variant | | `weightedDates` | `WeightedDateEntry[]` | ⚡ | Array of `{ date: Date, weight: number }` objects | | `numberOfMonths` | `number` | ❌ | Number of months to display (default: 1) | | `showOutsideDays` | `boolean` | ❌ | Show days from adjacent months (default: true) | > ⚡ You must provide either `datesPerVariant` OR `weightedDates`, not both. The component also accepts all props from [react-day-picker](https://react-day-picker.js.org/api/interfaces/DayPickerMultipleProps). ### Types ```typescript type WeightedDateEntry = { date: Date weight: number } ``` ## 🎨 Customization Examples ### Temperature Heatmap ```tsx const Heatmap = [ "text-white hover:text-white bg-blue-300 hover:bg-blue-300", // Cold "text-white hover:text-white bg-green-500 hover:bg-green-500", // Mild "text-white hover:text-white bg-amber-400 hover:bg-amber-400", // Warm "text-white hover:text-white bg-red-700 hover:bg-red-700", // Hot ] ``` ### Rainbow Colors ```tsx const Rainbow = [ "text-white hover:text-white bg-violet-400 hover:bg-violet-400", "text-white hover:text-white bg-indigo-400 hover:bg-indigo-400", "text-white hover:text-white bg-blue-400 hover:bg-blue-400", "text-white hover:text-white bg-green-400 hover:bg-green-400", "text-white hover:text-white bg-yellow-400 hover:bg-yellow-400", "text-white hover:text-white bg-orange-400 hover:bg-orange-400", "text-white hover:text-white bg-red-400 hover:bg-red-400", ] ``` ## ⭐ Star History Star History Chart ## 🤝 Contributing Contributions are welcome! Feel free to open an issue or submit a pull request. ## 📄 License MIT © [Gurbaaz Singh Nandra](https://x.com/GurbaazNandra) ## 🔗 Links - [Live Demo](https://shadcn-calendar-heatmap.vercel.app) - [GitHub Repository](https://github.com/gurbaaz27/shadcn-calendar-heatmap) - [Twitter/X](https://x.com/GurbaazNandra) ================================================ FILE: app/(components)/copy-llms-button.tsx ================================================ "use client" import { useState } from "react" import { buttonVariants } from "@/components/ui/button" import { cn } from "@/lib/utils" import { Check, Copy } from "lucide-react" interface CopyLlmsButtonProps { content: string className?: string } export function CopyLlmsButton({ content, className }: CopyLlmsButtonProps) { const [copied, setCopied] = useState(false) const handleCopy = async () => { await navigator.clipboard.writeText(content) setCopied(true) setTimeout(() => setCopied(false), 2000) } return ( ) } ================================================ FILE: app/(components)/example-code.tsx ================================================ import { Code } from "@/components/code" const tsx = `import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" // Github-style streak pattern // Or you may simply pass weighted array of dates, // and they would be slotted to different variants based on length of \`variantClassnames\` // Component code at https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx ` const code = `\`\`\`tsx /maxLength={6}/ /render/ /slots/1 /.map((slot, idx)/1 /Slot/2,3,4 /props.char/2 // ${tsx} \`\`\`` export function ExampleCode() { return (
{/* Anchor */}
) } ================================================ FILE: app/(components)/example-variants.ts ================================================ import { currentMonthFirstDate, currentMonthLastDate, randomDate, } from "@/lib/utils" export const GithubStreak = [ "text-white hover:text-white bg-green-400 hover:bg-green-400", "text-white hover:text-white bg-green-500 hover:bg-green-500", "text-white hover:text-white bg-green-700 hover:bg-green-700", ] export const GithubStreakDates = [ [...Array(12)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(3)) ), [...Array(9)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(3)) ), [...Array(6)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(3)) ), ] export const Heatmap = [ "text-white hover:text-white bg-blue-300 hover:bg-blue-300", "text-white hover:text-white bg-green-500 hover:bg-green-500", "text-white hover:text-white bg-amber-400 hover:bg-amber-400", "text-white hover:text-white bg-red-700 hover:bg-red-700", ] export const HeatmapDatesWeight = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] export const Rainbow = [ "text-white hover:text-white bg-violet-400 hover:bg-violet-400", "text-white hover:text-white bg-indigo-400 hover:bg-indigo-400", "text-white hover:text-white bg-blue-400 hover:bg-blue-400", "text-white hover:text-white bg-green-400 hover:bg-green-400", "text-white hover:text-white bg-yellow-400 hover:bg-yellow-400", "text-white hover:text-white bg-orange-400 hover:bg-orange-400", "text-white hover:text-white bg-red-400 hover:bg-red-400", ] export const RainbowDates = [ [...Array(3)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), [...Array(2)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), [...Array(1)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), [...Array(3)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), [...Array(2)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), [...Array(1)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), [...Array(3)].map((_) => randomDate(currentMonthFirstDate(), currentMonthLastDate(2)) ), ] ================================================ FILE: app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 240 10% 3.9%; --card: 0 0% 100%; --card-foreground: 240 10% 3.9%; --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; --primary: 240 5.9% 10%; --primary-foreground: 0 0% 98%; --secondary: 240 4.8% 95.9%; --secondary-foreground: 240 5.9% 10%; --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; --accent: 240 4.8% 95.9%; --accent-foreground: 240 5.9% 10%; --destructive: 0 72.22% 50.59%; --destructive-foreground: 0 0% 98%; --border: 240 5.9% 90%; --input: 240 5.9% 90%; --ring: 240 5% 64.9%; --radius: 0.5rem; } .dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; --card: 240 10% 3.9%; --card-foreground: 0 0% 98%; --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 240 5.9% 10%; --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; --accent: 240 3.7% 15.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 85.7% 97.3%; --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 240 4.9% 83.9%; } } @media (prefers-color-scheme: dark) { :root { --background: 240 10% 3.9%; --foreground: 0 0% 98%; --card: 240 10% 3.9%; --card-foreground: 0 0% 98%; --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 240 5.9% 10%; --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; --accent: 240 3.7% 15.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 85.7% 97.3%; --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 240 4.9% 83.9%; } } @layer base { * { @apply border-border; } body { /* @apply bg-background text-foreground selection:bg-[#6B2BF4] selection:text-foreground; */ @apply bg-background text-foreground; /* font-feature-settings: "rlig" 1, "calt" 1; */ font-synthesis-weight: none; text-rendering: optimizeLegibility; } } @layer utilities { } [data-highlighted-chars] { @apply bg-zinc-900 rounded; box-shadow: 2px 2px 0 2px rgba(139, 139, 148, 0.5); } [data-highlighted-chars] .dark { @apply bg-zinc-700/50 rounded; box-shadow: 2px 2px 0 2px rgba(139, 139, 148, 0.5); } [data-highlighted-chars] * { @apply !text-white; } [data-rehype-pretty-code-figure] pre { @apply pb-4 pt-6 max-h-[650px] overflow-x-auto rounded-lg border !bg-transparent; } [data-rehype-pretty-code-figure] [data-line] { @apply inline-block min-h-4 w-full py-0.5 px-4; } .code-example-overlay { background-image: linear-gradient( to bottom, theme("colors.background") 60%, transparent ); transform: translateY(0); animation: move-overlay 4s ease-out forwards; animation-delay: 3s; } .code-example-light { } .code-example-dark { display: none; } @media (prefers-color-scheme: dark) { .code-example-light { display: none; } .code-example-dark { display: unset; } } @media (prefers-reduced-motion: reduce) { .code-example-overlay { opacity: 0; animation: none; } } @keyframes move-overlay { 0% { transform: translateY(0); } 100% { transform: translateY(-100%); } } ================================================ FILE: app/layout.tsx ================================================ import "./globals.css" import type { Metadata } from "next" import { cn } from "@/lib/utils" import { GeistSans } from "geist/font/sans" import { JetBrains_Mono } from "next/font/google" import { SiteHeader } from "@/components/site-header" import { SiteFooter } from "@/components/site-footer" import { Toaster } from "@/components/ui/sonner" import { siteConfig } from "@/config/site" export const fontMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono", }) export const metadata: Metadata = { title: { default: siteConfig.title, template: `%s - ${siteConfig.name}`, }, metadataBase: new URL(siteConfig.url), description: siteConfig.description, keywords: [ "React", "Heatmap", "Calendar", "Next.js", "Tailwind CSS", "Server Components", "Accessible", "Shadcn", ], authors: [ { name: "gurbaaz", url: "https://gurbaaz.me", }, ], creator: "gurbaaz", openGraph: { type: "website", locale: "en_IN", url: siteConfig.url, title: siteConfig.name, description: siteConfig.description, siteName: siteConfig.name, images: [ { url: siteConfig.ogImage, width: 1200, height: 630, alt: siteConfig.name, }, ], }, twitter: { card: "summary_large_image", title: siteConfig.name, description: siteConfig.description, images: [siteConfig.ogImage], creator: "@GurbaazNandra", }, icons: { icon: "/favicon.ico", shortcut: "/favicon-16x16.png", apple: "/apple-touch-icon.png", }, } export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return (
{children}
) } ================================================ FILE: app/page.tsx ================================================ import { PageActions, PageHeader, PageHeaderDescription, PageHeaderHeading, PageHeaderNotifier, } from "@/components/page-header" import { buttonVariants } from "@/components/ui/button" import { siteConfig } from "@/config/site" import { cn, currentMonthFirstDate, currentMonthLastDate, randomDate, } from "@/lib/utils" import Link from "next/link" import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" import { Icons } from "@/components/icons" import { Star } from "lucide-react" import { ExampleCode } from "./(components)/example-code" import { CopyLlmsButton } from "./(components)/copy-llms-button" import { readFile } from "fs/promises" import path from "path" import { GithubStreak, GithubStreakDates, Heatmap, HeatmapDatesWeight, Rainbow, RainbowDates, } from "./(components)/example-variants" const fadeUpClassname = "lg:motion-safe:opacity-0 lg:motion-safe:animate-fade-up" async function getRepoStarCount() { const res = await fetch(`https://api.github.com/repos/${siteConfig.name}`) const data = await res.json() const starCount = data.stargazers_count if (starCount > 999) { return (starCount / 1000).toFixed(1) + "K" } return starCount } export default async function IndexPage() { const starCount = await getRepoStarCount() const llmsContent = await readFile( path.join(process.cwd(), "public", "llms.txt"), "utf-8" ) return (
Excited to officially launch our new shadcn-based component! 🎉 Modern alternative to primitive react heatmaps. Showcase Github streaks. Visualise user growth. Understand global warming trends.

Convey more with less.

Unstyled. Customizable. Open Source.
{siteConfig.name}
{starCount}
Examples Github Streaks Temperature Heatmap ({ date: randomDate(currentMonthFirstDate(), currentMonthLastDate()), weight: wgt, }))} /> Rainbow Colors
) } export const revalidate = 3600 ================================================ FILE: components/code.tsx ================================================ import * as React from "react" import { unified } from "unified" import remarkParse from "remark-parse" import remarkRehype from "remark-rehype" import rehypeStringify from "rehype-stringify" import rehypePrettyCode from "rehype-pretty-code" import { CopyButton } from "./copy-button" export async function Code({ code, toCopy, dark = true, }: { code: string toCopy?: string dark?: boolean }) { const highlightedCode = await highlightCode(code, dark) return (

      {toCopy && (
        
)}
) } async function highlightCode(code: string, dark: boolean) { const file = await unified() .use(remarkParse) .use(remarkRehype) .use(rehypePrettyCode, { keepBackground: false, theme: dark ? "vesper" : "github-light", }) .use(rehypeStringify) .process(code) return String(file) } ================================================ FILE: components/copy-button.tsx ================================================ // Stolen from @shadcn/ui the man the machine!! "use client" import * as React from "react" import { CheckIcon, CopyIcon } from "@radix-ui/react-icons" import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu" import { cn } from "@/lib/utils" import { Button } from "./ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./ui/dropdown-menu" interface CopyButtonProps extends React.HTMLAttributes { value: string src?: string } export async function copyToClipboardWithMeta(value: string) { window && window.isSecureContext && navigator.clipboard.writeText(value) } export function CopyButton({ value, className, src, ...props }: CopyButtonProps) { const [hasCopied, setHasCopied] = React.useState(false) React.useEffect(() => { setTimeout(() => { setHasCopied(false) }, 2000) }, [hasCopied]) return ( ) } interface CopyWithClassNamesProps extends DropdownMenuTriggerProps { value: string classNames: string className?: string } export function CopyWithClassNames({ value, classNames, className, ...props }: CopyWithClassNamesProps) { const [hasCopied, setHasCopied] = React.useState(false) React.useEffect(() => { setTimeout(() => { setHasCopied(false) }, 2000) }, [hasCopied]) const copyToClipboard = React.useCallback((value: string) => { copyToClipboardWithMeta(value) setHasCopied(true) }, []) return ( copyToClipboard(value)}> Component copyToClipboard(classNames)}> Classname ) } interface CopyNpmCommandButtonProps extends DropdownMenuTriggerProps { commands: { __npmCommand__: string __yarnCommand__: string __pnpmCommand__: string __bunCommand__: string } } export function CopyNpmCommandButton({ commands, className, ...props }: CopyNpmCommandButtonProps) { const [hasCopied, setHasCopied] = React.useState(false) React.useEffect(() => { setTimeout(() => { setHasCopied(false) }, 2000) }, [hasCopied]) const copyCommand = React.useCallback( (value: string, pm: "npm" | "pnpm" | "yarn" | "bun") => { copyToClipboardWithMeta(value) setHasCopied(true) }, [] ) return ( copyCommand(commands.__npmCommand__, "npm")} > npm copyCommand(commands.__yarnCommand__, "yarn")} > yarn copyCommand(commands.__pnpmCommand__, "pnpm")} > pnpm copyCommand(commands.__bunCommand__, "bun")} > bun ) } ================================================ FILE: components/icons.tsx ================================================ type IconProps = React.HTMLAttributes export const Icons = { logo: (props: IconProps) => ( ), twitter: (props: IconProps) => ( ), gitHub: (props: IconProps) => ( ), radix: (props: IconProps) => ( ), aria: (props: IconProps) => ( ), npm: (props: IconProps) => ( ), yarn: (props: IconProps) => ( ), pnpm: (props: IconProps) => ( ), react: (props: IconProps) => ( ), tailwind: (props: IconProps) => ( ), google: (props: IconProps) => ( ), apple: (props: IconProps) => ( ), paypal: (props: IconProps) => ( ), spinner: (props: IconProps) => ( ), } ================================================ FILE: components/page-header.tsx ================================================ import { cn } from "../lib/utils" function PageHeader({ className, children, ...props }: React.HTMLAttributes) { return (
{children}
) } function PageHeaderNotifier({ className, ...props }: React.HTMLAttributes) { return (

) } function PageHeaderHeading({ className, ...props }: React.HTMLAttributes) { return (

) } function PageHeaderDescription({ className, ...props }: React.HTMLAttributes) { return (

) } function PageActions({ className, ...props }: React.HTMLAttributes) { return (

) } export { PageHeader, PageHeaderNotifier, PageHeaderHeading, PageHeaderDescription, PageActions, } ================================================ FILE: components/site-footer.tsx ================================================ import { siteConfig } from "../config/site" export function SiteFooter() { return ( ) } ================================================ FILE: components/site-header.tsx ================================================ import Link from "next/link" import { siteConfig } from "../config/site" import { cn } from "../lib/utils" import { buttonVariants } from "./ui/button" import { Icons } from "./icons" export function SiteHeader() { return (
) } ================================================ FILE: components/ui/button.tsx ================================================ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "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", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) } ) Button.displayName = "Button" export { Button, buttonVariants } ================================================ FILE: components/ui/calendar-heatmap.tsx ================================================ "use client" import * as React from "react" import { ChevronLeft, ChevronRight } from "lucide-react" import { DayPicker } from "react-day-picker" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" // type utilities type UnionKeys = T extends T ? keyof T : never type Expand = T extends T ? { [K in keyof T]: T[K] } : never type OneOf = { [K in keyof T]: Expand< T[K] & Partial, keyof T[K]>, never>> > }[number] // types export type Classname = string export type WeightedDateEntry = { date: Date weight: number } interface IDatesPerVariant { datesPerVariant: Date[][] } interface IWeightedDatesEntry { weightedDates: WeightedDateEntry[] } type VariantDatesInput = OneOf<[IDatesPerVariant, IWeightedDatesEntry]> export type CalendarProps = React.ComponentProps & { variantClassnames: Classname[] } & VariantDatesInput /// utlity functions function useModifers( variantClassnames: Classname[], datesPerVariant: Date[][] ): [Record, Record] { const noOfVariants = variantClassnames.length const variantLabels = [...Array(noOfVariants)].map( (_, idx) => `__variant${idx}` ) const modifiers = variantLabels.reduce((acc, key, index) => { acc[key] = datesPerVariant[index] return acc }, {} as Record) const modifiersClassNames = variantLabels.reduce((acc, key, index) => { acc[key] = variantClassnames[index] return acc }, {} as Record) return [modifiers, modifiersClassNames] } function categorizeDatesPerVariant( weightedDates: WeightedDateEntry[], noOfVariants: number ) { const sortedEntries = weightedDates.sort((a, b) => a.weight - b.weight) const categorizedRecord = [...Array(noOfVariants)].map(() => [] as Date[]) const minNumber = sortedEntries[0].weight const maxNumber = sortedEntries[sortedEntries.length - 1].weight const range = minNumber == maxNumber ? 1 : (maxNumber - minNumber) / noOfVariants; sortedEntries.forEach((entry) => { const category = Math.min( Math.floor((entry.weight - minNumber) / range), noOfVariants - 1 ) categorizedRecord[category].push(entry.date) }) return categorizedRecord } function CalendarHeatmap({ variantClassnames, datesPerVariant, weightedDates, className, classNames, showOutsideDays = true, ...props }: CalendarProps) { const noOfVariants = variantClassnames.length weightedDates = weightedDates ?? [] datesPerVariant = datesPerVariant ?? categorizeDatesPerVariant(weightedDates, noOfVariants) const [modifiers, modifiersClassNames] = useModifers( variantClassnames, datesPerVariant ) return ( , IconRight: ({ ...props }) => , }} {...props} /> ) } CalendarHeatmap.displayName = "CalendarHeatmap" export { CalendarHeatmap } ================================================ FILE: components/ui/dropdown-menu.tsx ================================================ "use client" import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react" import { cn } from "@/lib/utils" const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, children, ...props }, ref) => ( {children} )) DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName const DropdownMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName const DropdownMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( )) DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, ...props }, ref) => ( )) DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, checked, ...props }, ref) => ( {children} )) DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )) DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, ...props }, ref) => ( )) DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ) } DropdownMenuShortcut.displayName = "DropdownMenuShortcut" export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, } ================================================ FILE: components/ui/select.tsx ================================================ "use client" import * as React from "react" import * as SelectPrimitive from "@radix-ui/react-select" import { Check, ChevronDown, ChevronUp } from "lucide-react" import { cn } from "@/lib/utils" const Select = SelectPrimitive.Root const SelectGroup = SelectPrimitive.Group const SelectValue = SelectPrimitive.Value const SelectTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( span]:line-clamp-1", className )} {...props} > {children} )) SelectTrigger.displayName = SelectPrimitive.Trigger.displayName const SelectScrollUpButton = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName const SelectScrollDownButton = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName const SelectContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, position = "popper", ...props }, ref) => ( {children} )) SelectContent.displayName = SelectPrimitive.Content.displayName const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectLabel.displayName = SelectPrimitive.Label.displayName const SelectItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )) SelectItem.displayName = SelectPrimitive.Item.displayName const SelectSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectSeparator.displayName = SelectPrimitive.Separator.displayName export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, } ================================================ FILE: components/ui/sonner.tsx ================================================ "use client" import { useTheme } from "next-themes" import { Toaster as Sonner } from "sonner" type ToasterProps = React.ComponentProps const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() return ( ) } export { Toaster } ================================================ FILE: components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } ================================================ FILE: config/site.ts ================================================ export const siteConfig = { title: "Shadcn Calendar Heatmap", name: "gurbaaz27/shadcn-calendar-heatmap", url: "https://shadcn-calendar-heatmap.vercel.app", ogImage: "https://shadcn-calendar-heatmap.vercel.app/og.png", description: "Accessible. Unstyled. Customizable. Open Source. Build your own calendar heatmap effortlessly.", links: { twitter: "https://x.com/GurbaazNandra", github: "https://github.com/gurbaaz27/shadcn-calendar-heatmap", }, } export type SiteConfig = typeof siteConfig ================================================ FILE: lib/utils.ts ================================================ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } export function randomDate(start: Date, end: Date) { return new Date( start.getTime() + Math.random() * (end.getTime() - start.getTime()) ) } export function currentMonthFirstDate() { const date = new Date() return new Date(date.getFullYear(), date.getMonth(), 1) } export function currentMonthLastDate(month: number = 1) { const date = new Date() return new Date(date.getFullYear(), date.getMonth() + month, 0) } ================================================ FILE: next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = {}; export default nextConfig; ================================================ FILE: package.json ================================================ { "name": "shadcn-calendar-heatmap", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", "geist": "^1.3.0", "lucide-react": "^0.396.0", "next": "14.2.4", "next-themes": "^0.3.0", "react": "^18", "react-day-picker": "^8.10.1", "react-dom": "^18", "rehype-pretty-code": "^0.13.2", "rehype-stringify": "^10.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "unified": "^11.0.5" }, "devDependencies": { "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "postcss": "^8", "tailwindcss": "^3.4.1", "eslint": "^8", "eslint-config-next": "14.2.4" }, "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" } ================================================ FILE: postcss.config.mjs ================================================ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {}, }, }; export default config; ================================================ FILE: public/llms.txt ================================================ # shadcn-calendar-heatmap > 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. ## Overview 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. ## Installation The component is designed to be copied directly into your project following the shadcn/ui philosophy. Copy the component from: https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx ### Dependencies - react-day-picker (^8.10.1) - tailwind-merge - class-variance-authority - lucide-react (for icons) ## Component API ### CalendarHeatmap Props The component extends all props from `react-day-picker`'s DayPicker, plus: | Prop | Type | Required | Description | |------|------|----------|-------------| | `variantClassnames` | `string[]` | Yes | Array of Tailwind CSS classes for each intensity level | | `datesPerVariant` | `Date[][]` | One of these | 2D array where each inner array contains dates for that variant | | `weightedDates` | `WeightedDateEntry[]` | One of these | Array of `{ date: Date, weight: number }` objects | | `numberOfMonths` | `number` | No | Number of months to display (default: 1) | | `showOutsideDays` | `boolean` | No | Show days from adjacent months (default: true) | **Note:** You must provide either `datesPerVariant` OR `weightedDates`, not both. ### WeightedDateEntry Type ```typescript type WeightedDateEntry = { date: Date weight: number } ``` ## Usage Examples ### Basic GitHub-style Contribution Graph ```tsx import { CalendarHeatmap } from "@/components/ui/calendar-heatmap" ``` ### Using Weighted Dates (Auto-categorization) When you have numeric data associated with dates, use `weightedDates`. The component automatically categorizes dates into variants based on their weights: ```tsx ``` ### Multi-month Display ```tsx ``` ### Rainbow Variant Example ```tsx const Rainbow = [ "text-white hover:text-white bg-violet-400 hover:bg-violet-400", "text-white hover:text-white bg-indigo-400 hover:bg-indigo-400", "text-white hover:text-white bg-blue-400 hover:bg-blue-400", "text-white hover:text-white bg-green-400 hover:bg-green-400", "text-white hover:text-white bg-yellow-400 hover:bg-yellow-400", "text-white hover:text-white bg-orange-400 hover:bg-orange-400", "text-white hover:text-white bg-red-400 hover:bg-red-400", ] ``` ## How It Works 1. **Variant Classes**: Each string in `variantClassnames` represents a color/style intensity level. The component creates DayPicker modifiers for each variant. 2. **Date Mapping**: - With `datesPerVariant`: Dates in the first array get the first variant class, second array gets second class, etc. - 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. 3. **Styling**: The component uses Tailwind CSS classes. Include both normal and hover states in your variant classes for consistent interaction feedback. ## Customization ### Custom Styling Override default calendar styles via the `classNames` prop: ```tsx ``` ### Additional Props Since the component extends DayPicker, you can use any DayPicker prop: ```tsx console.log(day)} /> ``` ## Links - Demo: https://shadcn-calendar-heatmap.vercel.app - GitHub: https://github.com/gurbaaz27/shadcn-calendar-heatmap - Component Source: https://github.com/gurbaaz27/shadcn-calendar-heatmap/blob/main/components/ui/calendar-heatmap.tsx ## License MIT ================================================ FILE: tailwind.config.ts ================================================ import type { Config } from "tailwindcss" const config = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, "fade-in": { from: { opacity: "0", }, to: { opacity: "1" }, }, "fade-up": { from: { opacity: "0", transform: "translateY(var(--fade-distance, .25rem))", }, to: { opacity: "1", transform: "translateY(0)" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", "fade-in": "fade-in 0.3s ease-out forwards", "fade-up": "fade-up 1s ease-out forwards", }, }, }, plugins: [require("tailwindcss-animate")], } satisfies Config export default config ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }