diff --git a/package.json b/package.json index b738320f5..82ba74d16 100644 --- a/package.json +++ b/package.json @@ -296,6 +296,7 @@ "@react-three/drei": "8.20.2", "@react-three/fiber": "7.0.25", "classnames": "2.5.1", + "date-fns": "^4.1.0", "dompurify": "^3.2.1", "ellipsed": "1.6.0", "i18next": "22.0.6", diff --git a/src/components/UploadButton/UploadButton.tsx b/src/components/UploadButton/UploadButton.tsx index 70994f98b..0823bf2ae 100644 --- a/src/components/UploadButton/UploadButton.tsx +++ b/src/components/UploadButton/UploadButton.tsx @@ -159,15 +159,15 @@ const FileUploadButton = ({ // Try parsing with full options first workbook = window.XLSX.read(arrayBuffer, { type: 'array', - cellFormula: false, // Disable formula parsing to avoid potential issues - cellNF: false, // Disable number format parsing - cellHTML: false, // Disable HTML parsing - cellText: true, // Force text output - cellDates: false, // Disable date parsing to avoid errors + cellFormula: true, + cellNF: true, + cellHTML: true, + cellText: true, + cellDates: true, error: (e: any) => { console.warn('Non-fatal XLSX error:', e); - }, // Log non-fatal errors - cellStyles: false, // Disable style parsing + }, + cellStyles: true, }); } catch (initialError) { console.warn( @@ -179,15 +179,15 @@ const FileUploadButton = ({ try { workbook = window.XLSX.read(arrayBuffer, { type: 'array', - sheetRows: 1000, // Limit number of rows to parse - cellFormula: false, - cellStyles: false, - bookDeps: false, // Don't parse external dependencies - bookFiles: false, // Don't parse embedded files - bookProps: false, // Don't parse document properties - bookSheets: false, // Don't parse sheet properties - bookVBA: false, // Don't parse VBA - WTF: true, // "What the Formula" mode - ignores errors when possible + sheetRows: 1000, + cellFormula: true, + cellStyles: true, + bookDeps: true, + bookFiles: true, + bookProps: true, + bookSheets: true, + bookVBA: true, + WTF: true, }); } catch (recoveryError) { setErrors(prev => [ @@ -247,27 +247,62 @@ const FileUploadButton = ({ ); } - // Try to convert sheet to CSV - let csv; + // Try to convert sheet to formatted text with columns + let formattedText; try { - csv = window.XLSX.utils.sheet_to_csv(worksheet); - } catch (csvError) { - // If CSV conversion fails, try a more basic cell-by-cell approach - csv = ''; + // Get array of arrays representation + const data = window.XLSX.utils.sheet_to_json(worksheet, { + header: 1, + raw: false, + }); + + // Find the maximum width for each column + const colWidths = data.reduce((widths: number[], row: any[]) => { + row.forEach((cell, i) => { + const cellWidth = (cell || '').toString().length; + widths[i] = Math.max(widths[i] || 0, cellWidth); + }); + return widths; + }, []); + + // Format each row with proper column spacing + formattedText = data.map((row: any[]) => { + return row + .map((cell, i) => { + const cellStr = (cell || '').toString(); + return cellStr.padEnd(colWidths[i] + 2); // Add 2 spaces padding + }) + .join('|') + .trim(); + }); + + // Add separator line after header + if (formattedText.length > 0) { + const separator = colWidths + .map((w: number) => '-'.repeat(w + 2)) + .join('+'); + formattedText.splice(1, 0, separator); + } + + formattedText = formattedText.join('\n'); + + } catch (formatError) { + // Fallback to basic formatting if advanced fails + formattedText = ''; for (let r = range.s.r; r <= Math.min(range.e.r, 1000); ++r) { let row = ''; for (let c = range.s.c; c <= Math.min(range.e.c, 100); ++c) { const cell = worksheet[window.XLSX.utils.encode_cell({ r: r, c: c })]; - row += (cell ? String(cell.v || '') : '') + ','; + row += (cell ? String(cell.v || '').padEnd(15) : ' '.repeat(15)) + '|'; } - csv += row + '\n'; + formattedText += row + '\n'; } - csv += '...(truncated due to potential corruption)'; + formattedText += '...(truncated due to potential corruption)'; } - // Add sheet name and content to final text - text += `Sheet: ${sheetName}\n${csv}\n\n`; + // Add sheet name and formatted content to final text + text += `Sheet: ${sheetName}\n${formattedText}\n\n`; successfulSheets++; } catch (sheetError) { // Log sheet-specific error but continue with other sheets @@ -291,7 +326,7 @@ const FileUploadButton = ({ text; } - return text; // Return the extracted text from all sheets + return text; } catch (error) { setErrors(prev => [ ...prev, @@ -303,7 +338,6 @@ const FileUploadButton = ({ fileId: file.name, }, ]); - // If any error occurs during processing, throw with descriptive message throw new Error( `XLSX extraction failed: ${ error instanceof Error ? error.message : 'Unknown error' diff --git a/src/components/ui/DatePicker.css b/src/components/ui/DatePicker.css new file mode 100644 index 000000000..0edb78785 --- /dev/null +++ b/src/components/ui/DatePicker.css @@ -0,0 +1,256 @@ +.memori-datepicker { + position: relative; + width: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} + +.memori-datepicker--label { + display: block; + margin-bottom: 0.5rem; + color: #374151; + font-weight: 500; +} + +.memori-datepicker--container { + position: relative; + width: 100%; +} + +.memori-datepicker--input-container { + position: relative; + display: flex; + align-items: center; +} + +.memori-datepicker--icon { + position: absolute; + z-index: 1; + left: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + color: #6b7280; +} + +.memori-datepicker--input-wrapper { + position: relative; + display: flex; + width: 100%; +} + +.memori-datepicker--input { + width: 100%; + padding: 0.5rem 2.5rem 0.5rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background-color: #fff; + color: #1f2937; + font-size: 0.875rem; + line-height: 1.5; + outline: none; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.memori-datepicker--input:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25); +} + +.memori-datepicker--input:disabled { + background-color: #f3f4f6; + cursor: not-allowed; + } + +.memori-datepicker--input-container:has(.memori-datepicker--icon) .memori-datepicker--input { + padding-left: 2.5rem; +} + + +.memori-datepicker--clear-button { + position: absolute; + top: 50%; + right: 0.5rem; + display: flex; + width: 1rem; + height: 1rem; + align-items: center; + justify-content: center; + padding: 0; + border: none; + background: none; + color: #6b7280; + cursor: pointer; + transform: translateY(-50%); +} + +.memori-datepicker--clear-button:hover { + color: #ef4444; +} + +.memori-datepicker--popup { + position: absolute; + z-index: 50; + left: 0; + overflow: auto; + max-width: calc(100vw - 1rem); + max-height: calc(100vh - 5rem); + border: 1px solid #e5e7eb; + border-radius: 0.375rem; + margin-top: 0.5rem; + background-color: #fff; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +.memori-datepicker--popup-end { + right: 0; + left: auto; +} + +.memori-datepicker--popup-content { + width: 18rem; + padding: 0.75rem; +} + +.memori-datepicker--header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.memori-datepicker--nav-button { + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + border: none; + border-radius: 0.25rem; + background: transparent; + color: #4b5563; + cursor: pointer; +} + +.memori-datepicker--nav-button:hover { + background-color: #f3f4f6; + color: #1f2937; +} + +.memori-datepicker--current-month { + color: #1f2937; + font-size: 0.875rem; + font-weight: 500; + text-align: center; +} + +.memori-datepicker--icon-svg { + width: 1rem; + height: 1rem; +} + +.memori-datepicker--days-header { + display: grid; + margin-bottom: 0.25rem; + grid-template-columns: repeat(7, 1fr); +} + +.memori-datepicker--day-name { + padding: 0.25rem 0; + color: #6b7280; + font-size: 0.75rem; + text-align: center; +} + +.memori-datepicker--days-grid { + display: grid; + gap: 1px; + grid-template-columns: repeat(7, 1fr); +} + +.memori-datepicker--day { + display: flex; + width: 100%; + height: 2rem; + align-items: center; + justify-content: center; + padding: 0.35rem; + border: none; + border-radius: 0.25rem; + background: transparent; + color: #1f2937; + cursor: pointer; + font-size: 0.75rem; +} + +.memori-datepicker--day:hover:not(.memori-datepicker--day-outside-month):not(.memori-datepicker--day-selected) { + background-color: #f3f4f6; +} + +.memori-datepicker--day-outside-month { + color: #9ca3af; +} + +.memori-datepicker--day-today { + font-weight: 600; +} + +.memori-datepicker--day-selected { + background-color: #3b82f6; + color: white; +} + +.memori-datepicker--time-select { + padding-top: 0.75rem; + border-top: 1px solid #e5e7eb; + margin-top: 0.75rem; +} + +.memori-datepicker--time-caption { + margin-bottom: 0.5rem; + color: #4b5563; + font-size: 0.875rem; + font-weight: 500; +} + +.memori-datepicker--time-container { + display: flex; + gap: 0.5rem; +} + +.memori-datepicker--time-column { + flex: 1; +} + +.memori-datepicker--time-label { + margin-bottom: 0.25rem; + color: #6b7280; + font-size: 0.75rem; + text-align: center; +} + +.memori-datepicker--time-options { + display: flex; + max-height: 10rem; + flex-direction: column; + border: 1px solid #e5e7eb; + border-radius: 0.25rem; + overflow-y: auto; +} + +.memori-datepicker--time-option { + padding: 0.25rem 0.5rem; + border: none; + background: transparent; + color: #1f2937; + cursor: pointer; + font-size: 0.75rem; + text-align: center; +} + +.memori-datepicker--time-option:hover:not(.memori-datepicker--time-option-selected) { + background-color: #f3f4f6; +} + +.memori-datepicker--time-option-selected { + background-color: #3b82f6; + color: white; +} \ No newline at end of file diff --git a/src/components/ui/DatePicker.stories.tsx b/src/components/ui/DatePicker.stories.tsx new file mode 100644 index 000000000..ea1887b2e --- /dev/null +++ b/src/components/ui/DatePicker.stories.tsx @@ -0,0 +1,222 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import DatePicker from './DatePicker'; +import './DatePicker.css'; + +// Custom calendar icon component +const CalendarIcon = () => ( + + + +); + +const meta: Meta = { + title: 'Components/DatePicker', + component: DatePicker, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], + argTypes: { + selected: { control: 'date' }, + onChange: { action: 'date changed' }, + locale: { + control: 'select', + options: ['en', 'it'], + description: 'The locale to use for formatting dates', + }, + showTimeSelect: { + control: 'boolean', + description: 'Whether to show time picker', + }, + timeIntervals: { + control: { type: 'number', min: 5, max: 60, step: 5 }, + description: 'The time intervals in minutes', + }, + calendarStartDay: { + control: { type: 'number', min: 0, max: 6 }, + description: 'First day of week (0 = Sunday, 1 = Monday)', + }, + dateFormat: { + control: 'text', + description: 'Date format string', + }, + popperPlacement: { + control: 'select', + options: ['bottom-start', 'bottom-end'], + description: 'Calendar placement', + }, + isClearable: { + control: 'boolean', + description: 'Whether the date can be cleared', + }, + disabled: { + control: 'boolean', + description: 'Whether the input is disabled', + }, + }, + args: { + onChange: (date) => console.log('Date changed:', date), + placeholderText: 'DD/MM/YYYY', + showTimeSelect: false, + calendarStartDay: 1, + timeIntervals: 15, + dateFormat: 'PP', + isClearable: false, + disabled: false, + locale: 'en', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +// Basic example +export const Basic: Story = { + args: {}, +}; + +// With default selected date +export const WithSelectedDate: Story = { + args: { + selected: new Date(), + }, +}; + +// With time selection +export const WithTimeSelect: Story = { + args: { + showTimeSelect: true, + selected: new Date(), + timeCaption: 'Time', + timeIntervals: 15, + }, +}; + +// With icon +export const WithIcon: Story = { + args: { + icon: , + }, +}; + +// Italian locale +export const ItalianLocale: Story = { + args: { + locale: 'it', + calendarStartDay: 1, + timeCaption: 'Ora', + placeholderText: 'GG/MM/AAAA', + }, +}; + +// Clearable +export const Clearable: Story = { + args: { + isClearable: true, + selected: new Date(), + }, +}; + +// Disabled +export const Disabled: Story = { + args: { + disabled: true, + selected: new Date(), + }, +}; + +// Right aligned popup +export const RightAligned: Story = { + args: { + popperPlacement: 'bottom-end', + }, +}; + +// With label +export const WithLabel: Story = { + args: { + label: 'Select Date', + }, +}; + +// Custom date format +export const CustomFormat: Story = { + args: { + dateFormat: 'dd/MM/yyyy HH:mm', + showTimeSelect: true, + selected: new Date(), + }, +}; + +// Interactive example with state hooks +export const Interactive: Story = { + render: (args) => { + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + + const handleStartDateChange = (date: Date | null) => { + setStartDate(date); + if (date && endDate && date > endDate) { + setEndDate(date); + } + }; + + const handleEndDateChange = (date: Date | null) => { + setEndDate(date); + }; + + return ( +
+
+ + } + isClearable + /> +
+ +
+ + } + isClearable + disabled={!startDate} + /> +
+ + {startDate && endDate && ( +
+

+ Selected Range: {startDate.toLocaleString()} to {endDate.toLocaleString()} +

+
+ )} +
+ ); + }, + parameters: { + docs: { + description: { + story: 'An interactive example showing date range selection with two DatePickers.', + }, + }, + }, +}; \ No newline at end of file diff --git a/src/components/ui/DatePicker.test.tsx b/src/components/ui/DatePicker.test.tsx new file mode 100644 index 000000000..0f0cef24a --- /dev/null +++ b/src/components/ui/DatePicker.test.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import DatePicker from './DatePicker'; +import { format } from 'date-fns'; + +// Mock the date so tests are predictable +const mockDate = new Date(2025, 3, 10); // April 10, 2025 +jest.useFakeTimers().setSystemTime(mockDate); + +describe('DatePicker', () => { + test('renders with default props', () => { + const handleChange = jest.fn(); + const { container } = render(); + + // Check that the component renders + const datepickerElement = container.querySelector('.memori-datepicker'); + expect(datepickerElement).not.toBeNull(); + + // Input should be rendered with the placeholder + const input = container.querySelector('input'); + expect(input).not.toBeNull(); + expect(input?.getAttribute('placeholder')).toBe('DD/MM/YYYY'); + }); + + test('renders with selected date', () => { + const handleChange = jest.fn(); + const selectedDate = new Date(2025, 3, 15); // April 15, 2025 + const { container } = render(); + + // Input should have the date value + const input = container.querySelector('input'); + expect(input).not.toBeNull(); + expect(input?.value).toBe(format(selectedDate, 'PP')); + }); + + test('applies custom className', () => { + const handleChange = jest.fn(); + const customClass = 'custom-datepicker'; + const { container } = render(); + + // Component should have the custom class + const datepickerElement = container.querySelector('.memori-datepicker'); + expect(datepickerElement).not.toBeNull(); + expect(datepickerElement?.classList.contains(customClass)).toBe(true); + }); + + test('renders with icon', () => { + const handleChange = jest.fn(); + const icon = icon; + const { container } = render(); + + // Icon should be rendered + const iconContainer = container.querySelector('.memori-datepicker--icon'); + expect(iconContainer).not.toBeNull(); + }); + + test('renders with label', () => { + const handleChange = jest.fn(); + const label = 'Test Label'; + const { container } = render(); + + // Label should be rendered + const labelElement = container.querySelector('.memori-datepicker--label'); + expect(labelElement).not.toBeNull(); + expect(labelElement?.textContent).toBe(label); + }); + + test('renders as disabled', () => { + const handleChange = jest.fn(); + const { container } = render(); + + // Input should be disabled + const input = container.querySelector('input'); + expect(input).not.toBeNull(); + expect(input?.disabled).toBe(true); + }); + + test('renders with clearable option when a date is selected', () => { + const handleChange = jest.fn(); + const { container } = render( + + ); + + // Clear button should be present + const clearButton = container.querySelector('.memori-datepicker--clear-button'); + expect(clearButton).not.toBeNull(); + }); + + test('renders with time selection options when showTimeSelect is true', () => { + const handleChange = jest.fn(); + const { container } = render( + + ); + + // Just check that the component renders successfully + const datepickerElement = container.querySelector('.memori-datepicker'); + expect(datepickerElement).not.toBeNull(); + }); + + test('renders with custom timeCaption', () => { + const handleChange = jest.fn(); + const customCaption = 'Custom Time'; + const { container } = render( + + ); + + // Just check that the component renders with the props + const datepickerElement = container.querySelector('.memori-datepicker'); + expect(datepickerElement).not.toBeNull(); + }); + + test('renders with custom popperPlacement', () => { + const handleChange = jest.fn(); + const { container } = render( + + ); + + // Component should render successfully + const datepickerElement = container.querySelector('.memori-datepicker'); + expect(datepickerElement).not.toBeNull(); + }); +}); \ No newline at end of file diff --git a/src/components/ui/DatePicker.tsx b/src/components/ui/DatePicker.tsx new file mode 100644 index 000000000..fe95b0154 --- /dev/null +++ b/src/components/ui/DatePicker.tsx @@ -0,0 +1,337 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import cx from 'classnames'; +import { Popover, Transition } from '@headlessui/react'; +import { format, parse, isValid, addDays, addMonths, subMonths, setHours, setMinutes, startOfMonth, endOfMonth, startOfWeek, endOfWeek, isSameMonth, isSameDay, addWeeks, Day } from 'date-fns'; +import { enUS, it } from 'date-fns/locale'; +import ChevronLeft from '../icons/ChevronLeft'; +import ChevronRight from '../icons/ChevronRight'; +import Clear from '../icons/Clear'; + +export interface Props { + name?: string; + className?: string; + placeholderText?: string; + selected?: Date | null; + onChange: (date: Date | null) => void; + locale?: 'en' | 'it'; + showTimeSelect?: boolean; + calendarStartDay?: number; + timeIntervals?: number; + timeCaption?: string; + dateFormat?: string; + icon?: React.ReactNode; + popperPlacement?: 'bottom-start' | 'bottom-end'; + isClearable?: boolean; + disabled?: boolean; + label?: string; +} + +const DatePicker = ({ + name, + className, + placeholderText = 'DD/MM/YYYY', + selected, + onChange, + locale = 'en', + showTimeSelect = false, + calendarStartDay = 0, + timeIntervals = 30, + timeCaption = 'Time', + dateFormat = 'PP', + icon, + popperPlacement = 'bottom-start', + isClearable = false, + disabled = false, + label, +}: Props) => { + const [currentMonth, setCurrentMonth] = useState(selected || new Date()); + const [selectedDate, setSelectedDate] = useState(selected || null); + const [selectedTime, setSelectedTime] = useState<{ hours: number; minutes: number } | null>( + selected ? { hours: selected.getHours(), minutes: selected.getMinutes() } : null + ); + const [inputValue, setInputValue] = useState(''); + + const localeObj = locale === 'it' ? it : enUS; + + useEffect(() => { + if (selected) { + setSelectedDate(selected); + setCurrentMonth(selected); + setSelectedTime(showTimeSelect ? { hours: selected.getHours(), minutes: selected.getMinutes() } : null); + setInputValue(formatDate(selected)); + } else { + setInputValue(''); + } + }, [selected, showTimeSelect]); + + const formatDate = (date: Date): string => { + if (!isValid(date)) return ''; + + try { + return format(date, dateFormat, { locale: localeObj }); + } catch (error) { + return format(date, 'PP', { locale: localeObj }); + } + }; + + const handleDateChange = (date: Date) => { + let newDate = date; + + if (selectedTime) { + newDate = setHours(newDate, selectedTime.hours); + newDate = setMinutes(newDate, selectedTime.minutes); + } + + setSelectedDate(newDate); + setInputValue(formatDate(newDate)); + onChange(newDate); + }; + + const handleTimeChange = (hours: number, minutes: number) => { + if (!selectedDate) return; + + const newTime = { hours, minutes }; + setSelectedTime(newTime); + + const newDate = setMinutes(setHours(selectedDate, hours), minutes); + setInputValue(formatDate(newDate)); + onChange(newDate); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + + // Try to parse the input value + if (value) { + try { + const parsedDate = parse(value, dateFormat, new Date(), { locale: localeObj }); + if (isValid(parsedDate)) { + setSelectedDate(parsedDate); + setCurrentMonth(parsedDate); + onChange(parsedDate); + return; + } + } catch (error) { + // Parsing failed, continue with setting just the input value + } + } + + // If parsing failed or value is empty + if (!value && isClearable) { + setSelectedDate(null); + onChange(null); + } + }; + + const handleClear = () => { + setSelectedDate(null); + setInputValue(''); + onChange(null); + }; + + const renderHeader = () => { + const monthName = format(currentMonth, 'MMMM yyyy', { locale: localeObj }); + + return ( +
+ + {monthName} + +
+ ); + }; + + const renderDays = () => { + const days = []; + let startDate = startOfWeek(startOfMonth(currentMonth), { weekStartsOn: calendarStartDay as Day }); + + // If we need to start from another day (e.g. Monday(1) instead of Sunday(0)) + if (calendarStartDay !== 0) { + startDate = startOfWeek(startOfMonth(currentMonth), { weekStartsOn: calendarStartDay as Day }); + } + + // Create day name headers + const dayNames = []; + for (let i = 0; i < 7; i++) { + const dayOfWeek = addDays(startDate, i); + dayNames.push( +
+ {format(dayOfWeek, 'EEEEEE', { locale: localeObj })} +
+ ); + } + + // Create calendar days + for (let i = 0; i < 42; i++) { + const day = addDays(startDate, i); + const isCurrentMonth = isSameMonth(day, currentMonth); + const isSelected = selectedDate ? isSameDay(day, selectedDate) : false; + const isToday = isSameDay(day, new Date()); + + days.push( + + ); + + // Break after we've rendered a full month + if (i === 41 || (i > 27 && !isCurrentMonth && isSameMonth(addDays(day, 1), endOfMonth(currentMonth)))) { + break; + } + } + + return ( +
+
{dayNames}
+
{days}
+
+ ); + }; + + const renderTimeSelect = () => { + if (!showTimeSelect) return null; + + const hours = []; + const minutes = []; + + // Generate hours + for (let i = 0; i < 24; i++) { + hours.push( + + ); + } + + // Generate minutes based on intervals + for (let i = 0; i < 60; i += timeIntervals) { + minutes.push( + + ); + } + + return ( +
+
{timeCaption}
+
+
+
Hours
+
{hours}
+
+
+
Minutes
+
{minutes}
+
+
+
+ ); + }; + + return ( +
+ {label && } + + {({ open }) => ( + <> +
+ {icon && {icon}} + + + {isClearable && inputValue && ( + + )} + +
+ + + +
+ {renderHeader()} + {renderDays()} + {renderTimeSelect()} +
+
+
+ + )} +
+
+ ); +}; + +export default DatePicker; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 734679d7c..6f18e5c19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4135,6 +4135,7 @@ __metadata: classnames: 2.5.1 copyfiles: 2.4.1 cssnano: 5.1.14 + date-fns: ^4.1.0 dompurify: ^3.2.1 ellipsed: 1.6.0 eslint: 8.29.0 @@ -10245,6 +10246,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^4.1.0": + version: 4.1.0 + resolution: "date-fns@npm:4.1.0" + checksum: fb681b242cccabed45494468f64282a7d375ea970e0adbcc5dcc92dcb7aba49b2081c2c9739d41bf71ce89ed68dd73bebfe06ca35129490704775d091895710b + languageName: node + linkType: hard + "dateformat@npm:^3.0.0": version: 3.0.3 resolution: "dateformat@npm:3.0.3"