Explorar o código

chore(dashboard): Update react-day-picker

Michael Bromley hai 8 meses
pai
achega
416d8cd4ec

+ 1 - 5
README.md

@@ -50,11 +50,7 @@ vendure/
 
 ### 1. Install top-level dependencies
 
-`npm install --legacy-peer-deps`
-
-Note: the `--legacy-peer-deps` flag is necessary because in the dashboard package there is a dependency "react-day-picker" (from Shadcn/ui) which
-has a peer dependency on React v18, whereas we are using v19 in this repo. The latest version of that library is however not currently
-compatible with shadcn (https://github.com/shadcn-ui/ui/issues/4366).
+`npm install`
 
 The root directory has a `package.json` which contains build-related dependencies for tasks including:
 

+ 36 - 6
package-lock.json

@@ -4088,6 +4088,7 @@
     },
     "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
       "version": "1.3.0",
+      "extraneous": true,
       "inBundle": true,
       "license": "MIT",
       "engines": {
@@ -4608,6 +4609,12 @@
         "node": ">=18"
       }
     },
+    "node_modules/@date-fns/tz": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
+      "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==",
+      "license": "MIT"
+    },
     "node_modules/@discoveryjs/json-ext": {
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
@@ -22692,6 +22699,12 @@
         "url": "https://opencollective.com/date-fns"
       }
     },
+    "node_modules/date-fns-jalali": {
+      "version": "4.1.0-0",
+      "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
+      "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
+      "license": "MIT"
+    },
     "node_modules/date-format": {
       "version": "4.0.14",
       "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
@@ -37808,17 +37821,34 @@
       }
     },
     "node_modules/react-day-picker": {
-      "version": "8.10.1",
-      "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
-      "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==",
+      "version": "9.6.7",
+      "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.6.7.tgz",
+      "integrity": "sha512-rCSt6X8FXQWpjykns/azRXjJk3cMSzkzGbDEXuEveFGNZgOjZULdJQ5wsu8Zfyo8ZgPBoYCBKQ5wRrgJfhJGbg==",
       "license": "MIT",
+      "dependencies": {
+        "@date-fns/tz": "1.2.0",
+        "date-fns": "4.1.0",
+        "date-fns-jalali": "4.1.0-0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
       "funding": {
         "type": "individual",
         "url": "https://github.com/sponsors/gpbl"
       },
       "peerDependencies": {
-        "date-fns": "^2.28.0 || ^3.0.0",
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/react-day-picker/node_modules/date-fns": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
       }
     },
     "node_modules/react-dom": {
@@ -47224,7 +47254,7 @@
         "motion": "^12.6.2",
         "next-themes": "^0.4.6",
         "react": "^19.0.0",
-        "react-day-picker": "^8.10.1",
+        "react-day-picker": "^9.6.7",
         "react-dom": "^19.0.0",
         "react-dropzone": "^14.3.8",
         "react-grid-layout": "^1.5.1",

+ 1 - 1
packages/dashboard/components.json

@@ -8,7 +8,7 @@
     "css": "src/styles.css",
     "baseColor": "neutral",
     "cssVariables": true,
-    "prefix": "",
+    "prefix": ""
   },
   "aliases": {
     "components": "@/components",

+ 3 - 3
packages/dashboard/package.json

@@ -100,7 +100,7 @@
         "motion": "^12.6.2",
         "next-themes": "^0.4.6",
         "react": "^19.0.0",
-        "react-day-picker": "^8.10.1",
+        "react-day-picker": "^9.6.7",
         "react-dom": "^19.0.0",
         "react-dropzone": "^14.3.8",
         "react-grid-layout": "^1.5.1",
@@ -110,10 +110,10 @@
         "tailwind-merge": "^3.0.1",
         "tailwindcss": "^4.0.6",
         "tailwindcss-animate": "^1.0.7",
+        "tsconfig-paths": "^4.2.0",
         "tw-animate-css": "^1.2.4",
         "vite": "^6.1.0",
-        "zod": "^3.24.2",
-        "tsconfig-paths": "^4.2.0"
+        "zod": "^3.24.2"
     },
     "devDependencies": {
         "@eslint/js": "^9.19.0",

+ 508 - 63
packages/dashboard/src/lib/components/ui/calendar.tsx

@@ -1,69 +1,514 @@
-import * as React from 'react';
-import { ChevronLeft, ChevronRight } from 'lucide-react';
-import { DayPicker } from 'react-day-picker';
+"use client"
 
-import { cn } from '@/lib/utils.js';
-import { buttonVariants } from '@/components/ui/button.js';
+// A custom calendar that is compatible
+// with react-day-picker v9 from https://date-picker.luca-felix.com/
 
+import { Button, buttonVariants } from "@/components/ui/button.js"
+import { cn } from "@/lib/utils.js"
+import { differenceInCalendarDays } from "date-fns"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import * as React from "react"
+import {
+  DayPicker,
+  labelNext,
+  labelPrevious,
+  useDayPicker,
+  type DayPickerProps,
+} from "react-day-picker"
+
+export type CalendarProps = DayPickerProps & {
+  /**
+   * In the year view, the number of years to display at once.
+   * @default 12
+   */
+  yearRange?: number
+
+  /**
+   * Wether to show the year switcher in the caption.
+   * @default true
+   */
+  showYearSwitcher?: boolean
+
+  monthsClassName?: string
+  monthCaptionClassName?: string
+  weekdaysClassName?: string
+  weekdayClassName?: string
+  monthClassName?: string
+  captionClassName?: string
+  captionLabelClassName?: string
+  buttonNextClassName?: string
+  buttonPreviousClassName?: string
+  navClassName?: string
+  monthGridClassName?: string
+  weekClassName?: string
+  dayClassName?: string
+  dayButtonClassName?: string
+  rangeStartClassName?: string
+  rangeEndClassName?: string
+  selectedClassName?: string
+  todayClassName?: string
+  outsideClassName?: string
+  disabledClassName?: string
+  rangeMiddleClassName?: string
+  hiddenClassName?: string
+}
+
+type NavView = "days" | "years"
+
+/**
+ * A custom calendar component built on top of react-day-picker.
+ * @param props The props for the calendar.
+ * @default yearRange 12
+ * @returns
+ */
 function Calendar({
-    className,
-    classNames,
-    showOutsideDays = true,
-    ...props
-}: React.ComponentProps<typeof DayPicker>) {
-    return (
-        <DayPicker
-            showOutsideDays={showOutsideDays}
-            className={cn('p-3', className)}
-            classNames={{
-                months: 'flex flex-col sm:flex-row gap-2',
-                month: 'flex flex-col gap-4',
-                caption: 'flex justify-center pt-1 relative items-center w-full',
-                caption_label: 'text-sm font-medium',
-                nav: 'flex items-center gap-1',
-                nav_button: cn(
-                    buttonVariants({ variant: 'outline' }),
-                    'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
-                ),
-                nav_button_previous: 'absolute left-1',
-                nav_button_next: 'absolute right-1',
-                table: 'w-full border-collapse space-x-1',
-                head_row: 'flex',
-                head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
-                row: 'flex w-full mt-2',
-                cell: cn(
-                    'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md',
-                    props.mode === 'range'
-                        ? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
-                        : '[&:has([aria-selected])]:rounded-md',
-                ),
-                day: cn(
-                    buttonVariants({ variant: 'ghost' }),
-                    'size-8 p-0 font-normal aria-selected:opacity-100',
-                ),
-                day_range_start:
-                    'day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground',
-                day_range_end: 'day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground',
-                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_outside: 'day-outside text-muted-foreground aria-selected:text-muted-foreground',
-                day_disabled: 'text-muted-foreground opacity-50',
-                day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
-                day_hidden: 'invisible',
-                ...classNames,
-            }}
-            components={{
-                IconLeft: ({ className, ...props }) => (
-                    <ChevronLeft className={cn('size-4', className)} {...props} />
-                ),
-                IconRight: ({ className, ...props }) => (
-                    <ChevronRight className={cn('size-4', className)} {...props} />
-                ),
-            }}
+  className,
+  showOutsideDays = true,
+  showYearSwitcher = true,
+  yearRange = 12,
+  numberOfMonths,
+  ...props
+}: CalendarProps) {
+  const [navView, setNavView] = React.useState<NavView>("days")
+  const [displayYears, setDisplayYears] = React.useState<{
+    from: number
+    to: number
+  }>(
+    React.useMemo(() => {
+      const currentYear = new Date().getFullYear()
+      return {
+        from: currentYear - Math.floor(yearRange / 2 - 1),
+        to: currentYear + Math.ceil(yearRange / 2),
+      }
+    }, [yearRange])
+  )
+
+  const { onNextClick, onPrevClick, startMonth, endMonth } = props
+
+  const columnsDisplayed = navView === "years" ? 1 : numberOfMonths
+
+  const _monthsClassName = cn("relative flex", props.monthsClassName)
+  const _monthCaptionClassName = cn(
+    "relative mx-10 flex h-7 items-center justify-center",
+    props.monthCaptionClassName
+  )
+  const _weekdaysClassName = cn("flex flex-row", props.weekdaysClassName)
+  const _weekdayClassName = cn(
+    "w-8 text-sm font-normal text-muted-foreground",
+    props.weekdayClassName
+  )
+  const _monthClassName = cn("w-full", props.monthClassName)
+  const _captionClassName = cn(
+    "relative flex items-center justify-center pt-1",
+    props.captionClassName
+  )
+  const _captionLabelClassName = cn(
+    "truncate text-sm font-medium",
+    props.captionLabelClassName
+  )
+  const buttonNavClassName = buttonVariants({
+    variant: "outline",
+    className:
+      "absolute h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
+  })
+  const _buttonNextClassName = cn(
+    buttonNavClassName,
+    "right-0",
+    props.buttonNextClassName
+  )
+  const _buttonPreviousClassName = cn(
+    buttonNavClassName,
+    "left-0",
+    props.buttonPreviousClassName
+  )
+  const _navClassName = cn("flex items-start", props.navClassName)
+  const _monthGridClassName = cn("mx-auto mt-4", props.monthGridClassName)
+  const _weekClassName = cn("mt-2 flex w-max items-start", props.weekClassName)
+  const _dayClassName = cn(
+    "flex size-8 flex-1 items-center justify-center p-0 text-sm",
+    props.dayClassName
+  )
+  const _dayButtonClassName = cn(
+    buttonVariants({ variant: "ghost" }),
+    "size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100",
+    props.dayButtonClassName
+  )
+  const buttonRangeClassName =
+    "bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground"
+  const _rangeStartClassName = cn(
+    buttonRangeClassName,
+    "day-range-start rounded-s-md",
+    props.rangeStartClassName
+  )
+  const _rangeEndClassName = cn(
+    buttonRangeClassName,
+    "day-range-end rounded-e-md",
+    props.rangeEndClassName
+  )
+  const _rangeMiddleClassName = cn(
+    "bg-accent !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground",
+    props.rangeMiddleClassName
+  )
+  const _selectedClassName = cn(
+    "[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground",
+    props.selectedClassName
+  )
+  const _todayClassName = cn(
+    "[&>button]:bg-accent [&>button]:text-accent-foreground",
+    props.todayClassName
+  )
+  const _outsideClassName = cn(
+    "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
+    props.outsideClassName
+  )
+  const _disabledClassName = cn(
+    "text-muted-foreground opacity-50",
+    props.disabledClassName
+  )
+  const _hiddenClassName = cn("invisible flex-1", props.hiddenClassName)
+
+  return (
+    <DayPicker
+      showOutsideDays={showOutsideDays}
+      className={cn("p-3", className)}
+      style={{
+        width: 248.8 * (columnsDisplayed ?? 1) + "px",
+      }}
+      classNames={{
+        months: _monthsClassName,
+        month_caption: _monthCaptionClassName,
+        weekdays: _weekdaysClassName,
+        weekday: _weekdayClassName,
+        month: _monthClassName,
+        caption: _captionClassName,
+        caption_label: _captionLabelClassName,
+        button_next: _buttonNextClassName,
+        button_previous: _buttonPreviousClassName,
+        nav: _navClassName,
+        month_grid: _monthGridClassName,
+        week: _weekClassName,
+        day: _dayClassName,
+        day_button: _dayButtonClassName,
+        range_start: _rangeStartClassName,
+        range_middle: _rangeMiddleClassName,
+        range_end: _rangeEndClassName,
+        selected: _selectedClassName,
+        today: _todayClassName,
+        outside: _outsideClassName,
+        disabled: _disabledClassName,
+        hidden: _hiddenClassName,
+      }}
+      components={{
+        Chevron: ({ orientation }) => {
+          const Icon = orientation === "left" ? ChevronLeft : ChevronRight
+          return <Icon className="h-4 w-4" />
+        },
+        Nav: ({ className }) => (
+          <Nav
+            className={className}
+            displayYears={displayYears}
+            navView={navView}
+            setDisplayYears={setDisplayYears}
+            startMonth={startMonth}
+            endMonth={endMonth}
+            onPrevClick={onPrevClick}
+          />
+        ),
+        CaptionLabel: (props) => (
+          <CaptionLabel
+            showYearSwitcher={showYearSwitcher}
+            navView={navView}
+            setNavView={setNavView}
+            displayYears={displayYears}
+            {...props}
+          />
+        ),
+        MonthGrid: ({ className, children, ...props }) => (
+          <MonthGrid
+            children={children}
+            className={className}
+            displayYears={displayYears}
+            startMonth={startMonth}
+            endMonth={endMonth}
+            navView={navView}
+            setNavView={setNavView}
             {...props}
-        />
-    );
+          />
+        ),
+      }}
+      numberOfMonths={columnsDisplayed}
+      {...props}
+    />
+  )
+}
+Calendar.displayName = "Calendar"
+
+function Nav({
+  className,
+  navView,
+  startMonth,
+  endMonth,
+  displayYears,
+  setDisplayYears,
+  onPrevClick,
+  onNextClick,
+}: {
+  className?: string
+  navView: NavView
+  startMonth?: Date
+  endMonth?: Date
+  displayYears: { from: number; to: number }
+  setDisplayYears: React.Dispatch<
+    React.SetStateAction<{ from: number; to: number }>
+  >
+  onPrevClick?: (date: Date) => void
+  onNextClick?: (date: Date) => void
+}) {
+  const { nextMonth, previousMonth, goToMonth } = useDayPicker()
+
+  const isPreviousDisabled = (() => {
+    if (navView === "years") {
+      return (
+        (startMonth &&
+          differenceInCalendarDays(
+            new Date(displayYears.from - 1, 0, 1),
+            startMonth
+          ) < 0) ||
+        (endMonth &&
+          differenceInCalendarDays(
+            new Date(displayYears.from - 1, 0, 1),
+            endMonth
+          ) > 0)
+      )
+    }
+    return !previousMonth
+  })()
+
+  const isNextDisabled = (() => {
+    if (navView === "years") {
+      return (
+        (startMonth &&
+          differenceInCalendarDays(
+            new Date(displayYears.to + 1, 0, 1),
+            startMonth
+          ) < 0) ||
+        (endMonth &&
+          differenceInCalendarDays(
+            new Date(displayYears.to + 1, 0, 1),
+            endMonth
+          ) > 0)
+      )
+    }
+    return !nextMonth
+  })()
+
+  const handlePreviousClick = React.useCallback(() => {
+    if (!previousMonth) return
+    if (navView === "years") {
+      setDisplayYears((prev) => ({
+        from: prev.from - (prev.to - prev.from + 1),
+        to: prev.to - (prev.to - prev.from + 1),
+      }))
+      onPrevClick?.(
+        new Date(
+          displayYears.from - (displayYears.to - displayYears.from),
+          0,
+          1
+        )
+      )
+      return
+    }
+    goToMonth(previousMonth)
+    onPrevClick?.(previousMonth)
+  }, [previousMonth, goToMonth])
+
+  const handleNextClick = React.useCallback(() => {
+    if (!nextMonth) return
+    if (navView === "years") {
+      setDisplayYears((prev) => ({
+        from: prev.from + (prev.to - prev.from + 1),
+        to: prev.to + (prev.to - prev.from + 1),
+      }))
+      onNextClick?.(
+        new Date(
+          displayYears.from + (displayYears.to - displayYears.from),
+          0,
+          1
+        )
+      )
+      return
+    }
+    goToMonth(nextMonth)
+    onNextClick?.(nextMonth)
+  }, [goToMonth, nextMonth])
+  return (
+    <nav className={cn("flex items-center", className)}>
+      <Button
+        variant="outline"
+        className="absolute left-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100"
+        type="button"
+        tabIndex={isPreviousDisabled ? undefined : -1}
+        disabled={isPreviousDisabled}
+        aria-label={
+          navView === "years"
+            ? `Go to the previous ${
+                displayYears.to - displayYears.from + 1
+              } years`
+            : labelPrevious(previousMonth)
+        }
+        onClick={handlePreviousClick}
+      >
+        <ChevronLeft className="h-4 w-4" />
+      </Button>
+
+      <Button
+        variant="outline"
+        className="absolute right-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100"
+        type="button"
+        tabIndex={isNextDisabled ? undefined : -1}
+        disabled={isNextDisabled}
+        aria-label={
+          navView === "years"
+            ? `Go to the next ${displayYears.to - displayYears.from + 1} years`
+            : labelNext(nextMonth)
+        }
+        onClick={handleNextClick}
+      >
+        <ChevronRight className="h-4 w-4" />
+      </Button>
+    </nav>
+  )
+}
+
+function CaptionLabel({
+  children,
+  showYearSwitcher,
+  navView,
+  setNavView,
+  displayYears,
+  ...props
+}: {
+  showYearSwitcher?: boolean
+  navView: NavView
+  setNavView: React.Dispatch<React.SetStateAction<NavView>>
+  displayYears: { from: number; to: number }
+} & React.HTMLAttributes<HTMLSpanElement>) {
+  if (!showYearSwitcher) return <span {...props}>{children}</span>
+  return (
+    <Button
+      className="h-7 w-full truncate text-sm font-medium"
+      variant="ghost"
+      size="sm"
+      onClick={() => setNavView((prev) => (prev === "days" ? "years" : "days"))}
+    >
+      {navView === "days"
+        ? children
+        : displayYears.from + " - " + displayYears.to}
+    </Button>
+  )
+}
+
+function MonthGrid({
+  className,
+  children,
+  displayYears,
+  startMonth,
+  endMonth,
+  navView,
+  setNavView,
+  ...props
+}: {
+  className?: string
+  children: React.ReactNode
+  displayYears: { from: number; to: number }
+  startMonth?: Date
+  endMonth?: Date
+  navView: NavView
+  setNavView: React.Dispatch<React.SetStateAction<NavView>>
+} & React.TableHTMLAttributes<HTMLTableElement>) {
+  if (navView === "years") {
+    return (
+      <YearGrid
+        displayYears={displayYears}
+        startMonth={startMonth}
+        endMonth={endMonth}
+        setNavView={setNavView}
+        navView={navView}
+        className={className}
+        {...props}
+      />
+    )
+  }
+  return (
+    <table className={className} {...props}>
+      {children}
+    </table>
+  )
+}
+
+function YearGrid({
+  className,
+  displayYears,
+  startMonth,
+  endMonth,
+  setNavView,
+  navView,
+  ...props
+}: {
+  className?: string
+  displayYears: { from: number; to: number }
+  startMonth?: Date
+  endMonth?: Date
+  setNavView: React.Dispatch<React.SetStateAction<NavView>>
+  navView: NavView
+} & React.HTMLAttributes<HTMLDivElement>) {
+  const { goToMonth, selected } = useDayPicker()
+
+  return (
+    <div className={cn("grid grid-cols-4 gap-y-2", className)} {...props}>
+      {Array.from(
+        { length: displayYears.to - displayYears.from + 1 },
+        (_, i) => {
+          const isBefore =
+            differenceInCalendarDays(
+              new Date(displayYears.from + i, 11, 31),
+              startMonth!
+            ) < 0
+
+          const isAfter =
+            differenceInCalendarDays(
+              new Date(displayYears.from + i, 0, 0),
+              endMonth!
+            ) > 0
+
+          const isDisabled = isBefore || isAfter
+          return (
+            <Button
+              key={i}
+              className={cn(
+                "h-7 w-full text-sm font-normal text-foreground",
+                displayYears.from + i === new Date().getFullYear() &&
+                  "bg-accent font-medium text-accent-foreground"
+              )}
+              variant="ghost"
+              onClick={() => {
+                setNavView("days")
+                goToMonth(
+                  new Date(
+                    displayYears.from + i,
+                    (selected as Date | undefined)?.getMonth() ?? 0
+                  )
+                )
+              }}
+              disabled={navView === "years" ? isDisabled : undefined}
+            >
+              {displayYears.from + i}
+            </Button>
+          )
+        }
+      )}
+    </div>
+  )
 }
 
-export { Calendar };
+export { Calendar }