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.**

## ✨ 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
## 🤝 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 (
{copied ? (
) : (
)}
{copied ? "Copied" : "Copy llms.txt"}
)
}
================================================
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 (
)
}
================================================
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 (
)
}
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 (
{
copyToClipboardWithMeta(value)
setHasCopied(true)
}}
{...props}
>
Copy
{hasCopied ? (
) : (
)}
)
}
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 (
{hasCopied ? (
) : (
)}
Copy
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 (
{hasCopied ? (
) : (
)}
Copy
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 (
)
}
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"]
}