rustlanges-components 0.1.0

RustLangES components library
Documentation
import { ArrowLeft, ArrowRight } from "@/icons";
import { Button } from "../button";
import { getSelectionHandler, WEEK_DAYS } from "./component.const";
import { cn } from "@/utils/tw-merge";
import { useMemo, useState } from "react";
import { getCalendarDays, addMonths, subMonths } from "@/utils/date";
import { CalendarDay } from "./calendar.day";
import { CalendarProps } from "./calendar.types";

/**
 * Calendar Component
 *
 * Renders an interactive monthly calendar that supports different selection modes: single date,
 * multiple dates, or a date range.
 *
 * Props:
 * @param {Date} [defaultMonth] - The initial month to display. Defaults to the current month.
 * @param {("single"|"multiple"|"range")} type - The selection mode defining how dates can be selected.
 * @param {Date | Record<string, boolean> | RangeDate | null} value - The current selection value matching the selection type.
 * @param {(value: Date | Record<string, boolean> | RangeDate | null) => void} onChange - Callback fired when the selection changes.
 *
 * Usage example:
 * ```tsx
 * <Calendar
 *   type="range"
 *   value={{ start: new Date(), end: null }}
 *   onChange={(range) => console.log("Selected range:", range)}
 *   defaultMonth={new Date()}
 * />
 * ```
 *
 * The internal `getSelectionHandler` function determines if a date is selected and how to update
 * the selection when a day is clicked, based on the selection type.
 *
 * @returns JSX.Element representing the calendar UI.
 */
export const Calendar = ({
  defaultMonth,
  ...rest
}: CalendarProps & { defaultMonth?: Date }) => {
  const [month, setMonth] = useState<Date>(defaultMonth ?? new Date());

  const { isSelected, onSelectDay } = useMemo(() => {
    return getSelectionHandler({
      ...rest,
    });
  }, [rest]);

  const calendarDays = useMemo(() => getCalendarDays(month), [month]);

  const handleNextMonth = () => setMonth(current => addMonths(current, 1));
  const handlePrevMonth = () => setMonth(current => subMonths(current, 1));

  const formattedYear = month.getFullYear();
  const formattedMonth = month.toLocaleDateString("es-MX", {
    month: "long",
  });

  return (
    <div
      className={cn([
        "shadow-rb-black grid gap-4 rounded-[20px] border px-3 pb-10 pt-4",
        "bg-light border-2 border-black text-black",
        "dark:bg-dark dark:border-neutral-950 dark:text-neutral-50",
      ])}
    >
      <div className="justify-between} flex items-center">
        <Button
          className="size-7 border-2"
          variant="icon"
          icon={<ArrowLeft />}
          onClick={handlePrevMonth}
        />
        <p className="flex-1 text-center text-sm font-medium capitalize">
          {formattedMonth} {formattedYear}
        </p>
        <Button
          className="size-7 border-2"
          variant="icon"
          icon={<ArrowRight />}
          onClick={handleNextMonth}
        />
      </div>
      <ul className="grid grid-cols-7 text-center" role="grid">
        {WEEK_DAYS.map(day => (
          <li className="mb-1.5 min-w-9 text-xs">{day.slice(0, 2)}</li>
        ))}
        {calendarDays.map(({ date, currentMonth }) => {
          const selected = isSelected(date);
          const disabled = !currentMonth;
          return (
            <li key={date.toISOString()}>
              <CalendarDay
                day={date}
                selected={selected}
                disabled={disabled}
                onSelectDay={onSelectDay}
              />
            </li>
          );
        })}
      </ul>
    </div>
  );
};