Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle error when weightedDates is an empty array #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 52 additions & 47 deletions components/ui/calendar-heatmap.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,92 @@
"use client"
"use client";

import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
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"
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";

// type utilities
type UnionKeys<T> = T extends T ? keyof T : never
type Expand<T> = T extends T ? { [K in keyof T]: T[K] } : never
type UnionKeys<T> = T extends T ? keyof T : never;
type Expand<T> = T extends T ? { [K in keyof T]: T[K] } : never;
type OneOf<T extends {}[]> = {
[K in keyof T]: Expand<
T[K] & Partial<Record<Exclude<UnionKeys<T[number]>, keyof T[K]>, never>>
>
}[number]
>;
}[number];

// types
export type Classname = string
export type Classname = string;
export type WeightedDateEntry = {
date: Date
weight: number
}
date: Date;
weight: number;
};

interface IDatesPerVariant {
datesPerVariant: Date[][]
datesPerVariant: Date[][];
}
interface IWeightedDatesEntry {
weightedDates: WeightedDateEntry[]
weightedDates: WeightedDateEntry[];
}

type VariantDatesInput = OneOf<[IDatesPerVariant, IWeightedDatesEntry]>
type VariantDatesInput = OneOf<[IDatesPerVariant, IWeightedDatesEntry]>;

export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
variantClassnames: Classname[]
} & VariantDatesInput
variantClassnames: Classname[];
showToday?: boolean;
} & VariantDatesInput;

/// utlity functions
function useModifers(
variantClassnames: Classname[],
datesPerVariant: Date[][]
): [Record<string, Date[]>, Record<string, string>] {
const noOfVariants = variantClassnames.length
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<string, Date[]>)
acc[key] = datesPerVariant[index];
return acc;
}, {} as Record<string, Date[]>);

const modifiersClassNames = variantLabels.reduce((acc, key, index) => {
acc[key] = variantClassnames[index]
return acc
}, {} as Record<string, string>)
acc[key] = variantClassnames[index];
return acc;
}, {} as Record<string, string>);

return [modifiers, modifiersClassNames]
return [modifiers, modifiersClassNames];
}

function categorizeDatesPerVariant(
weightedDates: WeightedDateEntry[],
noOfVariants: number
) {
const sortedEntries = weightedDates.sort((a, b) => a.weight - b.weight)
// Return array of empty arrays if weightedDates is empty
if (!weightedDates.length) {
return [...Array(noOfVariants)].map(() => [] as Date[]);
}

const categorizedRecord = [...Array(noOfVariants)].map(() => [] as Date[])
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;
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)
})
);
categorizedRecord[category].push(entry.date);
});

return categorizedRecord
return categorizedRecord;
}

function CalendarHeatmap({
Expand All @@ -90,18 +96,19 @@ function CalendarHeatmap({
className,
classNames,
showOutsideDays = true,
showToday = false,
...props
}: CalendarProps) {
const noOfVariants = variantClassnames.length
const noOfVariants = variantClassnames.length;

weightedDates = weightedDates ?? []
weightedDates = weightedDates ?? [];
datesPerVariant =
datesPerVariant ?? categorizeDatesPerVariant(weightedDates, noOfVariants)
datesPerVariant ?? categorizeDatesPerVariant(weightedDates, noOfVariants);

const [modifiers, modifiersClassNames] = useModifers(
variantClassnames,
datesPerVariant
)
);

return (
<DayPicker
Expand Down Expand Up @@ -132,9 +139,7 @@ function CalendarHeatmap({
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
// day_selected:
// "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_today: showToday ? "bg-accent text-accent-foreground" : "",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
Expand All @@ -149,8 +154,8 @@ function CalendarHeatmap({
}}
{...props}
/>
)
);
}
CalendarHeatmap.displayName = "CalendarHeatmap"
CalendarHeatmap.displayName = "CalendarHeatmap";

export { CalendarHeatmap }
export { CalendarHeatmap };