From 169e06d83cef5116f0e39c74d2c497f04ba6278d Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 29 May 2024 16:55:35 +0200 Subject: [PATCH 01/95] refactor: update isValidDate return type for improved type safety --- packages/time/src/tests/isValidDate.test.ts | 38 +++++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/time/src/tests/isValidDate.test.ts b/packages/time/src/tests/isValidDate.test.ts index 57a3556..188c31c 100644 --- a/packages/time/src/tests/isValidDate.test.ts +++ b/packages/time/src/tests/isValidDate.test.ts @@ -1,16 +1,32 @@ -import {describe, expect, test} from 'vitest'; -import {isValidDate} from '../utils/isValidDate'; +import { describe, expect, test } from 'vitest' +import { isValidDate } from '../utils/isValidDate' describe('isValidDate', () => { test('should return true for a valid date', () => { - expect(isValidDate(new Date())).toBe(true); - }); + const date = new Date(); + expect(isValidDate(date)).toBe(true) + }) - test('should return false for an invalid date', () => { - expect(isValidDate(new Date("invalid"))).toBe(false); - }); + test.each([ + '2021-10-10', + new Date('invalid'), + {}, + undefined, + null, + NaN, + 0, + ])('should return false for invalid date %p', (date) => { + expect(isValidDate(date)).toBe(false) + }) - test("should return false for null", () => { - expect(isValidDate(null)).toBe(false); - }); -}); \ No newline at end of file + test('should assert type guards correctly', () => { + const notADate = 'not a date'; + if (isValidDate(notADate)) { + expect(notADate).toBeInstanceOf(Date) + notADate.getDate() + } else { + // @ts-expect-error + notADate.getTime() + } + }) +}) From 75cb7b331a5c2ee41263f45509378197e9645b60 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 29 May 2024 16:55:47 +0200 Subject: [PATCH 02/95] refactor: update isValidDate return type for improved type safety --- packages/time/src/utils/isValidDate.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/time/src/utils/isValidDate.ts b/packages/time/src/utils/isValidDate.ts index 987df31..c0d1599 100644 --- a/packages/time/src/utils/isValidDate.ts +++ b/packages/time/src/utils/isValidDate.ts @@ -4,9 +4,6 @@ * @param date Date * @returns boolean */ -export function isValidDate(date: any): boolean { - if (Object.prototype.toString.call(date) !== '[object Date]') { - return false; - } - return date.getTime() === date.getTime(); -} \ No newline at end of file +export function isValidDate(date: unknown): date is Date { + return date instanceof Date && !isNaN(date.getTime()); +} From 5dca8d5e763a5e7cb2879ae836fec9cd16070a89 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 30 May 2024 17:52:05 +0200 Subject: [PATCH 03/95] refactor: update isValidDate return type for improved type safety --- packages/time/src/tests/isValidDate.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/time/src/tests/isValidDate.test.ts b/packages/time/src/tests/isValidDate.test.ts index 188c31c..4d01970 100644 --- a/packages/time/src/tests/isValidDate.test.ts +++ b/packages/time/src/tests/isValidDate.test.ts @@ -25,8 +25,10 @@ describe('isValidDate', () => { expect(notADate).toBeInstanceOf(Date) notADate.getDate() } else { - // @ts-expect-error - notADate.getTime() + expect(() => { + // @ts-expect-error + notADate.getTime() + }).toThrowError() } }) }) From e9e4be9c36c35283dded374d5475822fd888d203 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 30 May 2024 22:29:49 +0200 Subject: [PATCH 04/95] revert: isValidDate util --- packages/time/src/tests/isValidDate.test.ts | 38 ++++++--------------- packages/time/src/utils/isValidDate.ts | 9 +++-- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/packages/time/src/tests/isValidDate.test.ts b/packages/time/src/tests/isValidDate.test.ts index 4d01970..01afdc6 100644 --- a/packages/time/src/tests/isValidDate.test.ts +++ b/packages/time/src/tests/isValidDate.test.ts @@ -1,34 +1,16 @@ -import { describe, expect, test } from 'vitest' -import { isValidDate } from '../utils/isValidDate' +import {describe, expect, test} from 'vitest'; +import {isValidDate} from '../utils/isValidDate'; describe('isValidDate', () => { test('should return true for a valid date', () => { - const date = new Date(); - expect(isValidDate(date)).toBe(true) + expect(isValidDate(new Date())).toBe(true); }) - test.each([ - '2021-10-10', - new Date('invalid'), - {}, - undefined, - null, - NaN, - 0, - ])('should return false for invalid date %p', (date) => { - expect(isValidDate(date)).toBe(false) - }) + test('should return false for an invalid date', () => { + expect(isValidDate(new Date("invalid"))).toBe(false); + }); - test('should assert type guards correctly', () => { - const notADate = 'not a date'; - if (isValidDate(notADate)) { - expect(notADate).toBeInstanceOf(Date) - notADate.getDate() - } else { - expect(() => { - // @ts-expect-error - notADate.getTime() - }).toThrowError() - } - }) -}) + test("should return false for null", () => { + expect(isValidDate(null)).toBe(false); + }); +}); diff --git a/packages/time/src/utils/isValidDate.ts b/packages/time/src/utils/isValidDate.ts index c0d1599..987df31 100644 --- a/packages/time/src/utils/isValidDate.ts +++ b/packages/time/src/utils/isValidDate.ts @@ -4,6 +4,9 @@ * @param date Date * @returns boolean */ -export function isValidDate(date: unknown): date is Date { - return date instanceof Date && !isNaN(date.getTime()); -} +export function isValidDate(date: any): boolean { + if (Object.prototype.toString.call(date) !== '[object Date]') { + return false; + } + return date.getTime() === date.getTime(); +} \ No newline at end of file From d64d04a6a7bddb4feee5ed3af4736158aa3597a3 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sat, 1 Jun 2024 21:24:36 +0200 Subject: [PATCH 05/95] feat: useCalendar hook --- packages/react-time/package.json | 1 + packages/react-time/src/useCalendar.ts | 350 +++++++++++++++++++++++++ pnpm-lock.yaml | 64 +++-- 3 files changed, 389 insertions(+), 26 deletions(-) create mode 100644 packages/react-time/src/useCalendar.ts diff --git a/packages/react-time/package.json b/packages/react-time/package.json index c3f9d13..a784c85 100644 --- a/packages/react-time/package.json +++ b/packages/react-time/package.json @@ -62,6 +62,7 @@ "react-dom": "^17.0.0 || ^18.0.0" }, "dependencies": { + "@js-temporal/polyfill": "^0.4.4", "@tanstack/time": "workspace:*", "use-sync-external-store": "^1.2.0" }, diff --git a/packages/react-time/src/useCalendar.ts b/packages/react-time/src/useCalendar.ts new file mode 100644 index 0000000..e77606d --- /dev/null +++ b/packages/react-time/src/useCalendar.ts @@ -0,0 +1,350 @@ +import { useCallback, useMemo, useState } from 'react' +import { Temporal } from '@js-temporal/polyfill' +import type { CSSProperties, MouseEventHandler } from 'react' + +export interface Event { + id: string + startDate: Temporal.PlainDateTime + endDate: Temporal.PlainDateTime + title: string +} + +const getFirstDayOfMonth = (currMonth: string) => + Temporal.PlainDate.from(`${currMonth}-01`) + +const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { + const date = Temporal.PlainDate.from(currWeek) + return date.subtract({ days: (date.dayOfWeek - weekStartsOn + 7) % 7 }) +} + +const getChunks = function* (arr: T[], n: number) { + for (let i = 0; i < arr.length; i += n) { + yield arr.slice(i, i + n) + } +} + +const splitMultiDayEvents = (event: Event) => { + const startDate = Temporal.PlainDateTime.from(event.startDate) + const endDate = Temporal.PlainDateTime.from(event.endDate) + const events: Event[] = [] + + let currentDay = startDate + while ( + Temporal.PlainDate.compare( + currentDay.toPlainDate(), + endDate.toPlainDate(), + ) < 0 + ) { + const startOfCurrentDay = currentDay.with({ + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }) + const endOfCurrentDay = currentDay.with({ + hour: 23, + minute: 59, + second: 59, + millisecond: 999, + }) + + const eventStart = + Temporal.PlainDateTime.compare(currentDay, startDate) === 0 + ? startDate + : startOfCurrentDay + const eventEnd = + Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 + ? endDate + : endOfCurrentDay + + events.push({ + ...event, + startDate: eventStart, + endDate: eventEnd, + }) + + currentDay = startOfCurrentDay.add({ days: 1 }) + } + + return events +} + +interface UseCalendarProps { + weekStartsOn?: number + events: Event[] + viewMode: 'month' | 'week' | number + locale?: string + onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void +} + +const generateDateRange = ( + start: Temporal.PlainDate, + end: Temporal.PlainDate, +) => { + const dates: Temporal.PlainDate[] = [] + let current = start + while (Temporal.PlainDate.compare(current, end) <= 0) { + dates.push(current) + current = current.add({ days: 1 }) + } + return dates +} + +export const useCalendar = ({ + weekStartsOn = 1, + events, + viewMode: initialViewMode, + locale, + onChangeViewMode, +}: UseCalendarProps) => { + const today = Temporal.Now.plainDateISO() + + const [currPeriod, setCurrPeriod] = useState(today) + const [viewMode, setViewMode] = useState(initialViewMode) + + const firstDayOfMonth = getFirstDayOfMonth( + currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + ) + const firstDayOfWeek = getFirstDayOfWeek(currPeriod.toString(), weekStartsOn) + + const days = + viewMode === 'month' + ? Array.from( + getChunks( + generateDateRange( + firstDayOfMonth, + firstDayOfMonth.add({ months: 1 }).subtract({ days: 1 }), + ), + 7, + ), + ) + : viewMode === 'week' + ? Array.from( + getChunks( + generateDateRange( + firstDayOfWeek, + firstDayOfWeek.add({ days: 6 }), + ), + 7, + ), + ) + : Array.from( + getChunks( + generateDateRange( + currPeriod, + currPeriod.add({ days: viewMode - 1 }), + ), + viewMode, + ), + ) + + const eventMap = useMemo(() => { + const map = new Map() + + events.forEach((event) => { + const eventStartDate = Temporal.PlainDateTime.from(event.startDate) + const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + if ( + Temporal.PlainDate.compare( + eventStartDate.toPlainDate(), + eventEndDate.toPlainDate(), + ) !== 0 + ) { + const splitEvents = splitMultiDayEvents(event) + splitEvents.forEach((splitEvent) => { + const splitKey = splitEvent.startDate.toString().split('T')[0] + if (splitKey && !map.has(splitKey)) { + map.set(splitKey, []) + map.get(splitKey)?.push(splitEvent) + } + }) + } else { + const eventKey = event.startDate.toString().split('T')[0] + if (eventKey && !map.has(eventKey)) { + map.set(eventKey, []) + map.get(eventKey)?.push(event) + } + } + }) + + return map + }, [events]) + + const daysWithEvents = days.map((dayChunk) => { + return dayChunk.map((day) => { + const dayKey = day.toString() + const dailyEvents = eventMap.get(dayKey) ?? [] + + return { + date: day, + events: dailyEvents, + } + }) + }) + + const getPrev = useCallback>(() => { + switch (viewMode) { + case 'month': { + const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: 1 }) + setCurrPeriod(firstDayOfPrevMonth) + break + } + case 'week': { + const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: 1 }) + setCurrPeriod(firstDayOfPrevWeek) + break + } + default: { + const prevCustomStart = currPeriod.subtract({ days: viewMode }) + setCurrPeriod(prevCustomStart) + break + } + } + }, [viewMode, firstDayOfMonth, firstDayOfWeek, currPeriod]) + + const getNext = useCallback>(() => { + switch (viewMode) { + case 'month': { + const firstDayOfNextMonth = firstDayOfMonth.add({ months: 1 }) + setCurrPeriod(firstDayOfNextMonth) + break + } + case 'week': { + const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: 1 }) + setCurrPeriod(firstDayOfNextWeek) + break + } + default: { + const nextCustomStart = currPeriod.add({ days: viewMode }) + setCurrPeriod(nextCustomStart) + break + } + } + }, [viewMode, firstDayOfMonth, firstDayOfWeek, currPeriod]) + + const getCurrent = useCallback>(() => { + setCurrPeriod(today) + }, [today]) + + const get = useCallback((date: Temporal.PlainDate) => { + setCurrPeriod(date) + }, []) + + const chunks = + viewMode === 'month' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents] + const changeViewMode = useCallback( + (newViewMode: 'month' | 'week' | number) => { + onChangeViewMode?.(newViewMode) + setViewMode(newViewMode) + }, + [onChangeViewMode], + ) + + const getEventProps = useCallback( + (id: Event['id']): { style: CSSProperties } | null => { + const event = [...eventMap.values()] + .flat() + .find((event) => event.id === id) + if (!event) return null + + const eventStartDate = Temporal.PlainDateTime.from(event.startDate) + const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + + const isSplitEvent = + Temporal.PlainDate.compare( + eventStartDate.toPlainDate(), + eventEndDate.toPlainDate(), + ) !== 0 + + let percentageOfDay + let eventHeightInMinutes + + if (isSplitEvent) { + const isStartPart = + eventStartDate.hour !== 0 || eventStartDate.minute !== 0 + if (isStartPart) { + const eventTimeInMinutes = + eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + eventHeightInMinutes = 24 * 60 - eventTimeInMinutes + } else { + percentageOfDay = 0 + eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + } + } else { + const eventTimeInMinutes = + eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes + } + + const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) + + const overlappingEvents = [...eventMap.values()].flat().filter((e) => { + const eStartDate = Temporal.PlainDateTime.from(e.startDate) + const eEndDate = Temporal.PlainDateTime.from(e.endDate) + return ( + (e.id !== id && + Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) + ) + }) + + const eventIndex = overlappingEvents.findIndex((e) => e.id === id) + const totalOverlaps = overlappingEvents.length + const sidePadding = 2 + const innerPadding = 2 + const totalInnerPadding = (totalOverlaps - 1) * innerPadding + const availableWidth = 100 - totalInnerPadding - 2 * sidePadding + const eventWidth = + totalOverlaps > 0 + ? availableWidth / totalOverlaps + : 100 - 2 * sidePadding + const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) + + if (viewMode === 'week' || typeof viewMode === 'number') { + return { + style: { + position: 'absolute', + top: `min(${percentageOfDay}%, calc(100% - 55px))`, + left: `${eventLeft}%`, + width: `${eventWidth}%`, + margin: 0, + height: `${eventHeight}%`, + }, + } + } + + return null + }, + [eventMap, viewMode], + ) + + return { + firstDayOfPeriod: + viewMode === 'month' + ? firstDayOfMonth + : viewMode === 'week' + ? firstDayOfWeek + : currPeriod, + currPeriod: currPeriod.toString({ calendarName: 'auto' }), + getPrev, + getNext, + getCurrent, + get, + chunks, + daysNames: days + .flat() + .map((day) => day.toLocaleString(locale, { weekday: 'short' })), + viewMode, + changeViewMode, + getEventProps, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37c8fc7..e032657 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: packages/react-time: dependencies: + '@js-temporal/polyfill': + specifier: ^0.4.4 + version: 0.4.4 '@tanstack/time': specifier: workspace:* version: link:../time @@ -541,7 +544,7 @@ packages: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.2 - '@babel/generator': 7.23.6 + '@babel/generator': 7.24.1 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) '@babel/helpers': 7.24.1 @@ -2619,6 +2622,14 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@js-temporal/polyfill@0.4.4: + resolution: {integrity: sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==} + engines: {node: '>=12'} + dependencies: + jsbi: 4.3.0 + tslib: 2.6.2 + dev: false + /@leichtgewicht/ip-codec@2.0.5: resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} dev: true @@ -5113,7 +5124,7 @@ packages: dom-serializer: 2.0.0 domhandler: 5.0.3 htmlparser2: 8.0.2 - postcss: 8.4.35 + postcss: 8.4.38 postcss-media-query-parser: 0.2.3 dev: true @@ -5151,12 +5162,12 @@ packages: webpack: optional: true dependencies: - icss-utils: 5.1.0(postcss@8.4.35) - postcss: 8.4.35 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.35) - postcss-modules-local-by-default: 4.0.4(postcss@8.4.35) - postcss-modules-scope: 3.1.1(postcss@8.4.35) - postcss-modules-values: 4.0.0(postcss@8.4.35) + icss-utils: 5.1.0(postcss@8.4.38) + postcss: 8.4.38 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.38) + postcss-modules-local-by-default: 4.0.4(postcss@8.4.38) + postcss-modules-scope: 3.1.1(postcss@8.4.38) + postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 semver: 7.6.0 webpack: 5.90.3(esbuild@0.20.2) @@ -7006,13 +7017,13 @@ packages: safer-buffer: 2.1.2 dev: true - /icss-utils@5.1.0(postcss@8.4.35): + /icss-utils@5.1.0(postcss@8.4.38): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.38 dev: true /identity-function@1.0.0: @@ -7495,7 +7506,7 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.3 '@babel/parser': 7.24.1 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -7641,6 +7652,10 @@ packages: argparse: 2.0.1 dev: true + /jsbi@4.3.0: + resolution: {integrity: sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==} + dev: false + /jsdom@24.0.0: resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} engines: {node: '>=18'} @@ -7891,8 +7906,6 @@ packages: peerDependenciesMeta: webpack: optional: true - webpack-sources: - optional: true dependencies: webpack: 5.90.3(esbuild@0.20.2) webpack-sources: 3.2.3 @@ -9113,45 +9126,45 @@ packages: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} dev: true - /postcss-modules-extract-imports@3.0.0(postcss@8.4.35): + /postcss-modules-extract-imports@3.0.0(postcss@8.4.38): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.38 dev: true - /postcss-modules-local-by-default@4.0.4(postcss@8.4.35): + /postcss-modules-local-by-default@4.0.4(postcss@8.4.38): resolution: {integrity: sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.35) - postcss: 8.4.35 + icss-utils: 5.1.0(postcss@8.4.38) + postcss: 8.4.38 postcss-selector-parser: 6.0.16 postcss-value-parser: 4.2.0 dev: true - /postcss-modules-scope@3.1.1(postcss@8.4.35): + /postcss-modules-scope@3.1.1(postcss@8.4.38): resolution: {integrity: sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.38 postcss-selector-parser: 6.0.16 dev: true - /postcss-modules-values@4.0.0(postcss@8.4.35): + /postcss-modules-values@4.0.0(postcss@8.4.38): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.35) - postcss: 8.4.35 + icss-utils: 5.1.0(postcss@8.4.38) + postcss: 8.4.38 dev: true /postcss-selector-parser@6.0.16: @@ -9458,7 +9471,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.1 dev: true /regex-parser@2.3.0: @@ -9545,7 +9558,7 @@ packages: adjust-sourcemap-loader: 4.0.0 convert-source-map: 1.9.0 loader-utils: 2.0.4 - postcss: 8.4.35 + postcss: 8.4.38 source-map: 0.6.1 dev: true @@ -10741,7 +10754,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} From 28b43046ca47236c01941c0d139d3e51d37afdf9 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 2 Jun 2024 23:28:45 +0200 Subject: [PATCH 06/95] test: useCalendar tests --- packages/react-time/src/index.ts | 4 +- .../react-time/src/tests/useCalendar.test.ts | 99 +++++++++++++++++++ packages/react-time/src/useCalendar.ts | 2 +- packages/react-time/vite.config.ts | 4 +- 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 packages/react-time/src/tests/useCalendar.test.ts diff --git a/packages/react-time/src/index.ts b/packages/react-time/src/index.ts index dabac73..0b95ae9 100644 --- a/packages/react-time/src/index.ts +++ b/packages/react-time/src/index.ts @@ -1,3 +1 @@ -/** - * TanStack Time - */ \ No newline at end of file +export { useCalendar } from './useCalendar'; diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts new file mode 100644 index 0000000..d1548c4 --- /dev/null +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -0,0 +1,99 @@ +import { Temporal } from '@js-temporal/polyfill'; +import { describe, expect, test } from 'vitest'; +import { act, renderHook } from '@testing-library/react'; +import { useCalendar } from '../useCalendar'; + +describe('useCalendar', () => { + const events = [ + { + id: '1', + startDate: Temporal.PlainDateTime.from('2024-06-01T10:00:00'), + endDate: Temporal.PlainDateTime.from('2024-06-01T12:00:00'), + title: 'Event 1', + }, + { + id: '2', + startDate: Temporal.PlainDateTime.from('2024-06-02T14:00:00'), + endDate: Temporal.PlainDateTime.from('2024-06-02T16:00:00'), + title: 'Event 2', + }, + ]; + + const mockEvent = {} as React.MouseEvent; + + test('should initialize with the correct view mode and current period', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + expect(result.current.viewMode).toBe('month'); + expect(result.current.currPeriod).toBe(Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }).substring(0, 7)); + }); + + test('should navigate to the previous period correctly', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + + act(() => { + result.current.getPrev(mockEvent); + }); + + const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ months: 1 }); + expect(result.current.currPeriod).toBe(expectedPreviousMonth.toString({ calendarName: 'auto' }).substring(0, 7)); + }); + + test('should navigate to the next period correctly', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + + act(() => { + result.current.getNext(mockEvent); + }); + + const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }); + expect(result.current.currPeriod).toBe(expectedNextMonth.toString({ calendarName: 'auto' }).substring(0, 7)); + }); + + test('should reset to the current period correctly', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + + act(() => { + result.current.getNext(mockEvent); + result.current.getCurrent(mockEvent); + }); + + expect(result.current.currPeriod).toBe(Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }).substring(0, 7)); + }); + + test('should change view mode correctly', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + + act(() => { + result.current.changeViewMode('week'); + }); + + expect(result.current.viewMode).toBe('week'); + }); + + test('should select a day correctly', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + + act(() => { + result.current.get(Temporal.PlainDate.from('2024-06-01')); + }); + + expect(result.current.currPeriod).toBe('2024-06-01'); + }); + + test('should return the correct props for an event', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'week' })); + + const eventProps = result.current.getEventProps('1'); + + expect(eventProps).toEqual({ + style: { + position: 'absolute', + top: '41.66666666666667%', + left: '2%', + width: '96%', + margin: 0, + height: '5.555555555555555%', + }, + }); + }); +}); diff --git a/packages/react-time/src/useCalendar.ts b/packages/react-time/src/useCalendar.ts index e77606d..edfdf18 100644 --- a/packages/react-time/src/useCalendar.ts +++ b/packages/react-time/src/useCalendar.ts @@ -244,7 +244,7 @@ export const useCalendar = ({ (id: Event['id']): { style: CSSProperties } | null => { const event = [...eventMap.values()] .flat() - .find((event) => event.id === id) + .find((currEvent) => currEvent.id === id) if (!event) return null const eventStartDate = Temporal.PlainDateTime.from(event.startDate) diff --git a/packages/react-time/vite.config.ts b/packages/react-time/vite.config.ts index 68b8411..36d0656 100644 --- a/packages/react-time/vite.config.ts +++ b/packages/react-time/vite.config.ts @@ -1,11 +1,9 @@ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackBuildConfig } from '@tanstack/config/build' -import react from '@vitejs/plugin-react' const config = defineConfig({ - plugins: [react()], test: { - name: 'react-time', + name: 'time', dir: './src', watch: false, environment: 'jsdom', From 28f491c7375436be29e4e80cb325d1850cec06d0 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 2 Jun 2024 23:29:44 +0200 Subject: [PATCH 07/95] docs: useCalendar hook --- docs/framework/react/reference/uesCalendar.md | 65 +++++++++++++++++++ docs/framework/react/reference/useStore.md | 28 -------- 2 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 docs/framework/react/reference/uesCalendar.md delete mode 100644 docs/framework/react/reference/useStore.md diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/uesCalendar.md new file mode 100644 index 0000000..d798fc5 --- /dev/null +++ b/docs/framework/react/reference/uesCalendar.md @@ -0,0 +1,65 @@ +--- +title: Use Calendar +id: useCalendar +--- + +### `useCalendar` + +```tsx +export function useCalendar({ + weekStartsOn, + events, + viewMode, + locale, + onChangeViewMode, +}: UseCalendarProps) +``` + +`useCalendar` is a hook that provides a comprehensive set of functionalities for managing calendar events, view modes, and period navigation. + + +#### Parameters + +- `weekStartsOn?: number` + - This parameter is an optional number that specifies the day of the week that the calendar should start on. It defaults to 0, which is Sunday. +- `events: Event[]` + - This parameter is an array of events that the calendar should display. +- `viewMode: 'month' | 'week' | number` + - This parameter is a string that specifies the initial view mode of the calendar. It can be either 'month', 'week', or a number representing the number of days in a custom view mode. +- `locale?: string` + - This parameter is an optional string that specifies the locale to use for formatting dates and times. It defaults to the system locale. +- `onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void` + - This parameter is an optional callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. +- `onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void` + - This parameter is an optional callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. + + +#### Returns + +- `firstDayOfPeriod: Temporal.PlainDate` + - This value represents the first day of the current period displayed by the calendar. +- `currPeriod: string` + - This value represents a string that describes the current period displayed by the calendar. +- `getPrev: MouseEventHandler` + - This function is a click event handler that navigates to the previous period. +- `getNext: MouseEventHandler` + - This function is a click event handler that navigates to the next period. +- `getCurrent: MouseEventHandler` + - This function is a click event handler that navigates to the current period. +- `get: (date: Temporal.PlainDate) => void` + - This function is a callback function that is called when a date is selected in the calendar. It receives the selected date as an argument. +- `chunks: Array>` + - This value represents the calendar grid, where each cell contains the date and events for that day. +- `daysNames: string[]` + - This value represents an array of strings that contain the names of the days of the week. +- `viewMode: 'month' | 'week' | number` + - This value represents the current view mode of the calendar. +- `changeViewMode: (newViewMode: 'month' | 'week' | number) => void` + - This function is used to change the view mode of the calendar. +- `getEventProps: (id: string) => { style: CSSProperties } | null` + - This function is used to retrieve the style properties for a specific event based on its ID. +- `getEventProps: (id: string) => { style: CSSProperties } | null` + - This function is used to retrieve the style properties for a specific event based on its ID. +- `getEventProps: (id: string) => { style: CSSProperties } | null` + - This function is used to retrieve the style properties for a specific event based on its ID. + diff --git a/docs/framework/react/reference/useStore.md b/docs/framework/react/reference/useStore.md deleted file mode 100644 index 0a709c5..0000000 --- a/docs/framework/react/reference/useStore.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Use Store -id: useStore ---- -### `useStore` - -```tsx -export function useStore< - TState, - TSelected = NoInfer, - TUpdater extends AnyUpdater = AnyUpdater, ->( - store: Store, - selector: (state: NoInfer) => TSelected = (d) => d as any, -) -``` - -useStore is a custom hook that returns the updated store given the intial store and the selector. React can use this to keep your component subscribed to the store and re-render it on changes. - - - -#### Parameters -- `store: Store` - - This parameter represents the external store itself which holds the entire state of your application. It expects an instance of a `@tanstack/store` that manages state and supports updates through a callback. -- `selector: (state: NoInfer) => TSelected = (d) => d as any` - - This parameter is a callback function that takes the state of the store and expects you to return a sub-state of the store. This selected sub-state is subsequently utilized as the state displayed by the useStore hook. It triggers a re-render of the component only when there are changes in this data, ensuring that updates to the displayed state trigger the necessary re-renders - - From 468dc262149029341d79295598023323e350f095 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 2 Jun 2024 23:32:43 +0200 Subject: [PATCH 08/95] docs: useCalendar hook --- docs/framework/react/reference/uesCalendar.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/uesCalendar.md index d798fc5..91a0650 100644 --- a/docs/framework/react/reference/uesCalendar.md +++ b/docs/framework/react/reference/uesCalendar.md @@ -63,3 +63,79 @@ export function useCalendar({ - `getEventProps: (id: string) => { style: CSSProperties } | null` - This function is used to retrieve the style properties for a specific event based on its ID. + +#### Example Usage + +```tsx +const CalendarComponent = ({ events }) => { + const { + firstDayOfPeriod, + currPeriod, + getPrev, + getNext, + getCurrent, + get, + changeViewMode, + chunks, + daysNames, + viewMode, + getEventProps, + } = useCalendar({ + events, + viewMode: 'month', + locale: 'en-US', + onChangeViewMode: (newViewMode) => console.log('View mode changed:', newViewMode), + }); + + return ( +
+
+ + + +
+
+ + + + +
+
+ {viewMode === 'month' && ( +
+ {daysNames.map((dayName, index) => ( +
+ {dayName} +
+ ))} +
+ )} +
+ {chunks.map((week, weekIndex) => ( +
+ {week.map((day) => ( +
+
+ {day.date.day} +
+
+ {day.events.map((event) => ( +
+ {event.title} +
+ ))} +
+
+ ))} +
+ ))} +
+
+
+ ); +}; +``` \ No newline at end of file From 0b9bdeb6ee4818791da704c8ff6c0c914d03cde9 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 2 Jun 2024 23:39:32 +0200 Subject: [PATCH 09/95] test: useCalendar tests --- .../react-time/src/tests/useCalendar.test.ts | 115 +++++++++++------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts index d1548c4..c808afd 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -1,7 +1,7 @@ -import { Temporal } from '@js-temporal/polyfill'; -import { describe, expect, test } from 'vitest'; -import { act, renderHook } from '@testing-library/react'; -import { useCalendar } from '../useCalendar'; +import { Temporal } from '@js-temporal/polyfill' +import { describe, expect, test } from 'vitest' +import { act, renderHook } from '@testing-library/react' +import { useCalendar } from '../useCalendar' describe('useCalendar', () => { const events = [ @@ -17,83 +17,108 @@ describe('useCalendar', () => { endDate: Temporal.PlainDateTime.from('2024-06-02T16:00:00'), title: 'Event 2', }, - ]; + ] - const mockEvent = {} as React.MouseEvent; + const mockEvent = {} as React.MouseEvent test('should initialize with the correct view mode and current period', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); - expect(result.current.viewMode).toBe('month'); - expect(result.current.currPeriod).toBe(Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }).substring(0, 7)); - }); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month' }), + ) + expect(result.current.viewMode).toBe('month') + expect(result.current.currPeriod).toBe( + Temporal.Now.plainDateISO() + .toString({ calendarName: 'auto' }) + ) + }) test('should navigate to the previous period correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month' }), + ) act(() => { - result.current.getPrev(mockEvent); - }); + result.current.getPrev(mockEvent) + }) - const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ months: 1 }); - expect(result.current.currPeriod).toBe(expectedPreviousMonth.toString({ calendarName: 'auto' }).substring(0, 7)); - }); + const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ + months: 1, + }) + expect(result.current.currPeriod).toBe( + expectedPreviousMonth.toString({ calendarName: 'auto' }), + ) + }) test('should navigate to the next period correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month' }), + ) act(() => { - result.current.getNext(mockEvent); - }); + result.current.getNext(mockEvent) + }) - const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }); - expect(result.current.currPeriod).toBe(expectedNextMonth.toString({ calendarName: 'auto' }).substring(0, 7)); - }); + const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) + expect(result.current.currPeriod).toBe( + expectedNextMonth.toString({ calendarName: 'auto' }), + ) + }) test('should reset to the current period correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month' }), + ) act(() => { - result.current.getNext(mockEvent); - result.current.getCurrent(mockEvent); - }); + result.current.getNext(mockEvent) + result.current.getCurrent(mockEvent) + }) - expect(result.current.currPeriod).toBe(Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }).substring(0, 7)); - }); + expect(result.current.currPeriod).toBe( + Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }) + ) + }) test('should change view mode correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month' }), + ) act(() => { - result.current.changeViewMode('week'); - }); + result.current.changeViewMode('week') + }) - expect(result.current.viewMode).toBe('week'); - }); + expect(result.current.viewMode).toBe('week') + }) test('should select a day correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' })); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month' }), + ) act(() => { - result.current.get(Temporal.PlainDate.from('2024-06-01')); - }); + result.current.get(Temporal.PlainDate.from('2024-06-01')) + }) - expect(result.current.currPeriod).toBe('2024-06-01'); - }); + expect(result.current.currPeriod).toBe('2024-06-01') + }) test('should return the correct props for an event', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'week' })); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'week' }), + ) - const eventProps = result.current.getEventProps('1'); + const eventProps = result.current.getEventProps('1') expect(eventProps).toEqual({ style: { position: 'absolute', - top: '41.66666666666667%', + top: 'min(41.66666666666667%, calc(100% - 55px))', left: '2%', width: '96%', margin: 0, - height: '5.555555555555555%', + height: '8.333333333333332%', }, - }); - }); -}); + }) + }) +}) From 5b6533e812b615d8ff4bdfa510149dcb73c3e6a7 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 2 Jun 2024 23:44:06 +0200 Subject: [PATCH 10/95] test: useCalendar tests --- .../react-time/src/tests/useCalendar.test.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts index c808afd..e570172 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -41,12 +41,14 @@ describe('useCalendar', () => { result.current.getPrev(mockEvent) }) - const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ - months: 1, - }) - expect(result.current.currPeriod).toBe( - expectedPreviousMonth.toString({ calendarName: 'auto' }), - ) + const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ months: 1 }); + const expectedFirstDayOfPreviousMonth = Temporal.PlainDate.from({ + year: expectedPreviousMonth.year, + month: expectedPreviousMonth.month, + day: 1, + }); + + expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfPreviousMonth); }) test('should navigate to the next period correctly', () => { @@ -58,10 +60,14 @@ describe('useCalendar', () => { result.current.getNext(mockEvent) }) - const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) - expect(result.current.currPeriod).toBe( - expectedNextMonth.toString({ calendarName: 'auto' }), - ) + const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }); + const expectedFirstDayOfNextMonth = Temporal.PlainDate.from({ + year: expectedNextMonth.year, + month: expectedNextMonth.month, + day: 1, + }); + + expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfNextMonth); }) test('should reset to the current period correctly', () => { From 37effd4e7746d8595f6fa6b9e7f71fc85f800ae1 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 3 Jun 2024 00:12:17 +0200 Subject: [PATCH 11/95] test: overlaping events --- .../react-time/src/tests/useCalendar.test.ts | 66 ++++++++++++++++--- packages/react-time/src/useCalendar.ts | 12 ++-- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts index e570172..153db8e 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -27,8 +27,7 @@ describe('useCalendar', () => { ) expect(result.current.viewMode).toBe('month') expect(result.current.currPeriod).toBe( - Temporal.Now.plainDateISO() - .toString({ calendarName: 'auto' }) + Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }), ) }) @@ -41,14 +40,18 @@ describe('useCalendar', () => { result.current.getPrev(mockEvent) }) - const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ months: 1 }); + const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ + months: 1, + }) const expectedFirstDayOfPreviousMonth = Temporal.PlainDate.from({ year: expectedPreviousMonth.year, month: expectedPreviousMonth.month, day: 1, - }); + }) - expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfPreviousMonth); + expect(result.current.firstDayOfPeriod).toEqual( + expectedFirstDayOfPreviousMonth, + ) }) test('should navigate to the next period correctly', () => { @@ -60,14 +63,14 @@ describe('useCalendar', () => { result.current.getNext(mockEvent) }) - const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }); + const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) const expectedFirstDayOfNextMonth = Temporal.PlainDate.from({ year: expectedNextMonth.year, month: expectedNextMonth.month, day: 1, - }); + }) - expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfNextMonth); + expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfNextMonth) }) test('should reset to the current period correctly', () => { @@ -81,7 +84,7 @@ describe('useCalendar', () => { }) expect(result.current.currPeriod).toBe( - Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }) + Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }), ) }) @@ -127,4 +130,49 @@ describe('useCalendar', () => { }, }) }) + + test('should return the correct props for overlapping events', () => { + const overlappingEvents = [ + { + id: '1', + startDate: Temporal.PlainDateTime.from('2024-06-01T10:00:00'), + endDate: Temporal.PlainDateTime.from('2024-06-01T12:00:00'), + title: 'Event 1', + }, + { + id: '2', + startDate: Temporal.PlainDateTime.from('2024-06-01T11:00:00'), + endDate: Temporal.PlainDateTime.from('2024-06-01T13:00:00'), + title: 'Event 2', + }, + ] + const { result } = renderHook(() => + useCalendar({ events: overlappingEvents, viewMode: 'week' }), + ) + + const event1Props = result.current.getEventProps('1') + const event2Props = result.current.getEventProps('2') + + expect(event1Props).toEqual({ + style: { + position: 'absolute', + top: 'min(41.66666666666667%, calc(100% - 55px))', + left: '2%', + width: '47%', + margin: 0, + height: '8.333333333333332%', + }, + }) + + expect(event2Props).toEqual({ + style: { + position: 'absolute', + top: 'min(45.83333333333333%, calc(100% - 55px))', + left: '51%', + width: '47%', + margin: 0, + height: '8.333333333333332%', + }, + }) + }) }) diff --git a/packages/react-time/src/useCalendar.ts b/packages/react-time/src/useCalendar.ts index edfdf18..22fbf4c 100644 --- a/packages/react-time/src/useCalendar.ts +++ b/packages/react-time/src/useCalendar.ts @@ -153,16 +153,16 @@ export const useCalendar = ({ const splitEvents = splitMultiDayEvents(event) splitEvents.forEach((splitEvent) => { const splitKey = splitEvent.startDate.toString().split('T')[0] - if (splitKey && !map.has(splitKey)) { - map.set(splitKey, []) - map.get(splitKey)?.push(splitEvent) + if (splitKey) { + if (!map.has(splitKey)) map.set(splitKey, []) + map.get(splitKey)?.push(splitEvent) } }) } else { const eventKey = event.startDate.toString().split('T')[0] - if (eventKey && !map.has(eventKey)) { - map.set(eventKey, []) - map.get(eventKey)?.push(event) + if (eventKey) { + if (!map.has(eventKey)) map.set(eventKey, []) + map.get(eventKey)?.push(event) } } }) From 096f95d82d64c964dddaf59fee79b8c694fc0433 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 3 Jun 2024 16:36:36 +0200 Subject: [PATCH 12/95] feat(react-time/useCalendar): getCurrentTimeMarkerProps --- docs/framework/react/reference/uesCalendar.md | 2 ++ .../react-time/src/tests/useCalendar.test.ts | 23 +++++++++++++++- packages/react-time/src/useCalendar.ts | 27 ++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/uesCalendar.md index 91a0650..4fbf0aa 100644 --- a/docs/framework/react/reference/uesCalendar.md +++ b/docs/framework/react/reference/uesCalendar.md @@ -62,6 +62,8 @@ export function useCalendar({ - This function is used to retrieve the style properties for a specific event based on its ID. - `getEventProps: (id: string) => { style: CSSProperties } | null` - This function is used to retrieve the style properties for a specific event based on its ID. +- `getCurrentTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` + - This function is used to retrieve the style properties and current time for the current time marker. #### Example Usage diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts index 153db8e..2c29fe5 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -1,5 +1,5 @@ import { Temporal } from '@js-temporal/polyfill' -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useCalendar } from '../useCalendar' @@ -175,4 +175,25 @@ describe('useCalendar', () => { }, }) }) + + test('should return the correct props for the current time marker', () => { + vi.setSystemTime(new Date('2024-06-01T11:00:00')); + + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'week' }), + ) + + const currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps() + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.83333333333333%', + left: '0%', + }, + currentTime: '11:00', + }) + + vi.useRealTimers(); + }) }) diff --git a/packages/react-time/src/useCalendar.ts b/packages/react-time/src/useCalendar.ts index 22fbf4c..1bc7b87 100644 --- a/packages/react-time/src/useCalendar.ts +++ b/packages/react-time/src/useCalendar.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Temporal } from '@js-temporal/polyfill' import type { CSSProperties, MouseEventHandler } from 'react' @@ -101,6 +101,7 @@ export const useCalendar = ({ const [currPeriod, setCurrPeriod] = useState(today) const [viewMode, setViewMode] = useState(initialViewMode) + const [currentTime, setCurrentTime] = useState(Temporal.Now.plainDateTimeISO()); const firstDayOfMonth = getFirstDayOfMonth( currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), @@ -327,6 +328,29 @@ export const useCalendar = ({ [eventMap, viewMode], ) + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentTime(Temporal.Now.plainDateTimeISO()); + }, 60000); + + return () => clearInterval(intervalId); + }, []); + + const getCurrentTimeMarkerProps = useCallback(() => { + const { hour, minute } = currentTime; + const currentTimeInMinutes = hour * 60 + minute; + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; + + return { + style: { + position: 'absolute', + top: `${percentageOfDay}%`, + left: 0, + }, + currentTime: currentTime.toString().split('T')[1]?.substring(0, 5), + } + }, [currentTime]); + return { firstDayOfPeriod: viewMode === 'month' @@ -346,5 +370,6 @@ export const useCalendar = ({ viewMode, changeViewMode, getEventProps, + getCurrentTimeMarkerProps, } } From 26b0e1e675d44bf914867419a3c9e9e8f992f381 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 3 Jun 2024 16:43:17 +0200 Subject: [PATCH 13/95] feat(react-time/useCalendar): getCurrentTimeMarkerProps --- docs/framework/react/reference/uesCalendar.md | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/uesCalendar.md index 4fbf0aa..60563c1 100644 --- a/docs/framework/react/reference/uesCalendar.md +++ b/docs/framework/react/reference/uesCalendar.md @@ -82,6 +82,7 @@ const CalendarComponent = ({ events }) => { daysNames, viewMode, getEventProps, + getCurrentTimeMarkerProps, } = useCalendar({ events, viewMode: 'month', @@ -102,21 +103,23 @@ const CalendarComponent = ({ events }) => { -
+ {viewMode === 'month' && ( -
- {daysNames.map((dayName, index) => ( -
- {dayName} -
- ))} -
+ + + {daysNames.map((dayName, index) => ( + + ))} + + )} -
+
{chunks.map((week, weekIndex) => ( -
+
{week.map((day) => ( -
+
))} - +
+ ))} - - + +
+ {dayName} +
{day.date.day}
@@ -131,12 +134,13 @@ const CalendarComponent = ({ events }) => { ))} - +
); }; From 3cf89c309fadcaef581fdb93e9eb6ad7f955a1dd Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 3 Jun 2024 16:47:09 +0200 Subject: [PATCH 14/95] feat(react-time/useCalendar): getCurrentTimeMarkerProps --- docs/framework/react/reference/uesCalendar.md | 2 +- packages/react-time/src/useCalendar.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/uesCalendar.md index 60563c1..3ac395a 100644 --- a/docs/framework/react/reference/uesCalendar.md +++ b/docs/framework/react/reference/uesCalendar.md @@ -144,4 +144,4 @@ const CalendarComponent = ({ events }) => { ); }; -``` \ No newline at end of file +``` diff --git a/packages/react-time/src/useCalendar.ts b/packages/react-time/src/useCalendar.ts index 1bc7b87..5d14ba4 100644 --- a/packages/react-time/src/useCalendar.ts +++ b/packages/react-time/src/useCalendar.ts @@ -243,6 +243,9 @@ export const useCalendar = ({ const getEventProps = useCallback( (id: Event['id']): { style: CSSProperties } | null => { + // TODO: Drag and drop events + // TODO: Change event duration by dragging + const event = [...eventMap.values()] .flat() .find((currEvent) => currEvent.id === id) From 4ae3c4547b0821f81a8529e0629af26adc68832c Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 3 Jun 2024 22:59:12 +0200 Subject: [PATCH 15/95] refactor: reducer --- packages/react-time/package.json | 1 + .../react-time/src/tests/useCalendar.test.ts | 2 +- .../src/useCalendar/calendarActions.ts | 10 ++ packages/react-time/src/useCalendar/index.ts | 1 + .../src/{ => useCalendar}/useCalendar.ts | 156 ++++++++++-------- .../src/useCalendar/useCalendarReducer.ts | 29 ++++ .../src/useCalendar/useCalendarState.ts | 14 ++ 7 files changed, 144 insertions(+), 69 deletions(-) create mode 100644 packages/react-time/src/useCalendar/calendarActions.ts create mode 100644 packages/react-time/src/useCalendar/index.ts rename packages/react-time/src/{ => useCalendar}/useCalendar.ts (76%) create mode 100644 packages/react-time/src/useCalendar/useCalendarReducer.ts create mode 100644 packages/react-time/src/useCalendar/useCalendarState.ts diff --git a/packages/react-time/package.json b/packages/react-time/package.json index a784c85..0181e8d 100644 --- a/packages/react-time/package.json +++ b/packages/react-time/package.json @@ -64,6 +64,7 @@ "dependencies": { "@js-temporal/polyfill": "^0.4.4", "@tanstack/time": "workspace:*", + "typesafe-actions": "^5.1.0", "use-sync-external-store": "^1.2.0" }, "devDependencies": { diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts index 2c29fe5..a425434 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -189,7 +189,7 @@ describe('useCalendar', () => { style: { position: 'absolute', top: '45.83333333333333%', - left: '0%', + left: 0, }, currentTime: '11:00', }) diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts new file mode 100644 index 0000000..c713b37 --- /dev/null +++ b/packages/react-time/src/useCalendar/calendarActions.ts @@ -0,0 +1,10 @@ +import { createAction } from 'typesafe-actions'; +import type { Temporal } from '@js-temporal/polyfill'; +import type { ActionType } from 'typesafe-actions'; + +export const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); +export const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); +export const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); + +const actions = { setCurrentPeriod, setViewMode, updateCurrentTime }; +export type CalendarAction = ActionType; diff --git a/packages/react-time/src/useCalendar/index.ts b/packages/react-time/src/useCalendar/index.ts new file mode 100644 index 0000000..0b95ae9 --- /dev/null +++ b/packages/react-time/src/useCalendar/index.ts @@ -0,0 +1 @@ +export { useCalendar } from './useCalendar'; diff --git a/packages/react-time/src/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts similarity index 76% rename from packages/react-time/src/useCalendar.ts rename to packages/react-time/src/useCalendar/useCalendar.ts index 5d14ba4..06b9c73 100644 --- a/packages/react-time/src/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,12 +1,20 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { Temporal } from '@js-temporal/polyfill' +import { + setCurrentPeriod, + setViewMode, + updateCurrentTime, +} from './calendarActions' +import { useCalendarReducer } from './useCalendarReducer' +import type { Event } from './useCalendarState' import type { CSSProperties, MouseEventHandler } from 'react' -export interface Event { - id: string - startDate: Temporal.PlainDateTime - endDate: Temporal.PlainDateTime - title: string +interface UseCalendarProps { + weekStartsOn?: number + events: Event[] + viewMode: 'month' | 'week' | number + locale?: string + onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void } const getFirstDayOfMonth = (currMonth: string) => @@ -69,14 +77,6 @@ const splitMultiDayEvents = (event: Event) => { return events } -interface UseCalendarProps { - weekStartsOn?: number - events: Event[] - viewMode: 'month' | 'week' | number - locale?: string - onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void -} - const generateDateRange = ( start: Temporal.PlainDate, end: Temporal.PlainDate, @@ -98,18 +98,21 @@ export const useCalendar = ({ onChangeViewMode, }: UseCalendarProps) => { const today = Temporal.Now.plainDateISO() - - const [currPeriod, setCurrPeriod] = useState(today) - const [viewMode, setViewMode] = useState(initialViewMode) - const [currentTime, setCurrentTime] = useState(Temporal.Now.plainDateTimeISO()); - + const [state, dispatch] = useCalendarReducer({ + currPeriod: today, + viewMode: initialViewMode, + currentTime: Temporal.Now.plainDateTimeISO(), + }) const firstDayOfMonth = getFirstDayOfMonth( - currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + ) + const firstDayOfWeek = getFirstDayOfWeek( + state.currPeriod.toString(), + weekStartsOn, ) - const firstDayOfWeek = getFirstDayOfWeek(currPeriod.toString(), weekStartsOn) const days = - viewMode === 'month' + state.viewMode === 'month' ? Array.from( getChunks( generateDateRange( @@ -119,7 +122,7 @@ export const useCalendar = ({ 7, ), ) - : viewMode === 'week' + : state.viewMode === 'week' ? Array.from( getChunks( generateDateRange( @@ -132,10 +135,10 @@ export const useCalendar = ({ : Array.from( getChunks( generateDateRange( - currPeriod, - currPeriod.add({ days: viewMode - 1 }), + state.currPeriod, + state.currPeriod.add({ days: state.viewMode - 1 }), ), - viewMode, + state.viewMode, ), ) @@ -156,14 +159,14 @@ export const useCalendar = ({ const splitKey = splitEvent.startDate.toString().split('T')[0] if (splitKey) { if (!map.has(splitKey)) map.set(splitKey, []) - map.get(splitKey)?.push(splitEvent) + map.get(splitKey)?.push(splitEvent) } }) } else { const eventKey = event.startDate.toString().split('T')[0] if (eventKey) { if (!map.has(eventKey)) map.set(eventKey, []) - map.get(eventKey)?.push(event) + map.get(eventKey)?.push(event) } } }) @@ -184,68 +187,85 @@ export const useCalendar = ({ }) const getPrev = useCallback>(() => { - switch (viewMode) { + switch (state.viewMode) { case 'month': { const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: 1 }) - setCurrPeriod(firstDayOfPrevMonth) + dispatch(setCurrentPeriod(firstDayOfPrevMonth)) break } case 'week': { const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: 1 }) - setCurrPeriod(firstDayOfPrevWeek) + dispatch(setCurrentPeriod(firstDayOfPrevWeek)) break } default: { - const prevCustomStart = currPeriod.subtract({ days: viewMode }) - setCurrPeriod(prevCustomStart) + const prevCustomStart = state.currPeriod.subtract({ + days: state.viewMode, + }) + dispatch(setCurrentPeriod(prevCustomStart)) break } } - }, [viewMode, firstDayOfMonth, firstDayOfWeek, currPeriod]) + }, [ + state.viewMode, + state.currPeriod, + firstDayOfMonth, + dispatch, + firstDayOfWeek, + ]) const getNext = useCallback>(() => { - switch (viewMode) { + switch (state.viewMode) { case 'month': { const firstDayOfNextMonth = firstDayOfMonth.add({ months: 1 }) - setCurrPeriod(firstDayOfNextMonth) + dispatch(setCurrentPeriod(firstDayOfNextMonth)) break } case 'week': { const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: 1 }) - setCurrPeriod(firstDayOfNextWeek) + dispatch(setCurrentPeriod(firstDayOfNextWeek)) break } default: { - const nextCustomStart = currPeriod.add({ days: viewMode }) - setCurrPeriod(nextCustomStart) + const nextCustomStart = state.currPeriod.add({ days: state.viewMode }) + dispatch(setCurrentPeriod(nextCustomStart)) break } } - }, [viewMode, firstDayOfMonth, firstDayOfWeek, currPeriod]) + }, [ + state.viewMode, + state.currPeriod, + firstDayOfMonth, + dispatch, + firstDayOfWeek, + ]) const getCurrent = useCallback>(() => { - setCurrPeriod(today) - }, [today]) + dispatch(setCurrentPeriod(Temporal.Now.plainDateISO())) + }, [dispatch]) - const get = useCallback((date: Temporal.PlainDate) => { - setCurrPeriod(date) - }, []) + const get = useCallback( + (date: Temporal.PlainDate) => { + dispatch(setCurrentPeriod(date)) + }, + [dispatch], + ) const chunks = - viewMode === 'month' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents] + state.viewMode === 'month' + ? [...getChunks(daysWithEvents, 7)] + : [daysWithEvents] + const changeViewMode = useCallback( (newViewMode: 'month' | 'week' | number) => { onChangeViewMode?.(newViewMode) - setViewMode(newViewMode) + dispatch(setViewMode(newViewMode)) }, - [onChangeViewMode], + [dispatch, onChangeViewMode], ) const getEventProps = useCallback( (id: Event['id']): { style: CSSProperties } | null => { - // TODO: Drag and drop events - // TODO: Change event duration by dragging - const event = [...eventMap.values()] .flat() .find((currEvent) => currEvent.id === id) @@ -313,7 +333,7 @@ export const useCalendar = ({ : 100 - 2 * sidePadding const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - if (viewMode === 'week' || typeof viewMode === 'number') { + if (state.viewMode === 'week' || typeof state.viewMode === 'number') { return { style: { position: 'absolute', @@ -328,21 +348,21 @@ export const useCalendar = ({ return null }, - [eventMap, viewMode], + [eventMap, state.viewMode], ) useEffect(() => { const intervalId = setInterval(() => { - setCurrentTime(Temporal.Now.plainDateTimeISO()); - }, 60000); - - return () => clearInterval(intervalId); - }, []); + dispatch(updateCurrentTime(Temporal.Now.plainDateTimeISO())) + }, 60000) + + return () => clearInterval(intervalId) + }, [dispatch]) const getCurrentTimeMarkerProps = useCallback(() => { - const { hour, minute } = currentTime; - const currentTimeInMinutes = hour * 60 + minute; - const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; + const { hour, minute } = state.currentTime + const currentTimeInMinutes = hour * 60 + minute + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 return { style: { @@ -350,18 +370,18 @@ export const useCalendar = ({ top: `${percentageOfDay}%`, left: 0, }, - currentTime: currentTime.toString().split('T')[1]?.substring(0, 5), + currentTime: state.currentTime.toString().split('T')[1]?.substring(0, 5), } - }, [currentTime]); + }, [state.currentTime]) return { firstDayOfPeriod: - viewMode === 'month' + state.viewMode === 'month' ? firstDayOfMonth - : viewMode === 'week' + : state.viewMode === 'week' ? firstDayOfWeek - : currPeriod, - currPeriod: currPeriod.toString({ calendarName: 'auto' }), + : state.currPeriod, + currPeriod: state.currPeriod.toString({ calendarName: 'auto' }), getPrev, getNext, getCurrent, @@ -370,7 +390,7 @@ export const useCalendar = ({ daysNames: days .flat() .map((day) => day.toLocaleString(locale, { weekday: 'short' })), - viewMode, + viewMode: state.viewMode, changeViewMode, getEventProps, getCurrentTimeMarkerProps, diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts new file mode 100644 index 0000000..06391a4 --- /dev/null +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -0,0 +1,29 @@ +import { useReducer } from 'react'; +import { createReducer } from 'typesafe-actions'; + +import { setCurrentPeriod, setViewMode, updateCurrentTime } from './calendarActions'; +import type { CalendarAction} from './calendarActions'; +import type { CalendarState} from './useCalendarState'; + +const createCalendarReducer = (initialState: CalendarState) => { + return createReducer(initialState) + .handleAction(setCurrentPeriod, (state, action) => ({ + ...state, + currPeriod: action.payload, + })) + .handleAction(setViewMode, (state, action) => ({ + ...state, + viewMode: action.payload, + })) + .handleAction(updateCurrentTime, (state, action) => ({ + ...state, + currentTime: action.payload, + })); +} + +export const useCalendarReducer = ( + initialState: TState, +) => { + const reducer = createCalendarReducer(initialState); + return useReducer(reducer, initialState); +} diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts new file mode 100644 index 0000000..4f7eb91 --- /dev/null +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -0,0 +1,14 @@ +import type { Temporal } from '@js-temporal/polyfill'; + +export interface Event { + id: string; + startDate: Temporal.PlainDateTime; + endDate: Temporal.PlainDateTime; + title: string; +} + +export interface CalendarState { + currPeriod: Temporal.PlainDate; + viewMode: 'month' | 'week' | number; + currentTime: Temporal.PlainDateTime; +} From 4009dae1481028b0bb81b3f9e4df10f2c981f152 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 3 Jun 2024 22:59:20 +0200 Subject: [PATCH 16/95] refactor: reducer --- pnpm-lock.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e032657..8df55dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: react-dom: specifier: ^17.0.0 || ^18.0.0 version: 18.2.0(react@18.2.0) + typesafe-actions: + specifier: ^5.1.0 + version: 5.1.0 use-sync-external-store: specifier: ^1.2.0 version: 1.2.0(react@18.2.0) @@ -10838,6 +10841,11 @@ packages: resolution: {integrity: sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==} dev: true + /typesafe-actions@5.1.0: + resolution: {integrity: sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg==} + engines: {node: '>= 4'} + dev: false + /typescript@4.9.3: resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} engines: {node: '>=4.2.0'} From fa3716a5c06882540739e797211c36a9627d5e02 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 4 Jun 2024 11:52:33 +0200 Subject: [PATCH 17/95] refactor(useCalendar): types --- packages/react-time/src/useCalendar/useCalendar.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 06b9c73..2f6b300 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -9,9 +9,9 @@ import { useCalendarReducer } from './useCalendarReducer' import type { Event } from './useCalendarState' import type { CSSProperties, MouseEventHandler } from 'react' -interface UseCalendarProps { +interface UseCalendarProps { weekStartsOn?: number - events: Event[] + events: TEvent[] viewMode: 'month' | 'week' | number locale?: string onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void @@ -31,10 +31,10 @@ const getChunks = function* (arr: T[], n: number) { } } -const splitMultiDayEvents = (event: Event) => { +const splitMultiDayEvents = (event: TEvent): TEvent[] => { const startDate = Temporal.PlainDateTime.from(event.startDate) const endDate = Temporal.PlainDateTime.from(event.endDate) - const events: Event[] = [] + const events: TEvent[] = [] let currentDay = startDate while ( @@ -90,13 +90,13 @@ const generateDateRange = ( return dates } -export const useCalendar = ({ +export const useCalendar = ({ weekStartsOn = 1, events, viewMode: initialViewMode, locale, onChangeViewMode, -}: UseCalendarProps) => { +}: UseCalendarProps) => { const today = Temporal.Now.plainDateISO() const [state, dispatch] = useCalendarReducer({ currPeriod: today, @@ -143,7 +143,7 @@ export const useCalendar = ({ ) const eventMap = useMemo(() => { - const map = new Map() + const map = new Map() events.forEach((event) => { const eventStartDate = Temporal.PlainDateTime.from(event.startDate) From 131984b16eb26d26b810a2093f9a066c3958a164 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 4 Jun 2024 12:04:35 +0200 Subject: [PATCH 18/95] refactor(useCalendar): types --- packages/react-time/src/useCalendar/useCalendar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 2f6b300..89ae6b8 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -13,7 +13,7 @@ interface UseCalendarProps { weekStartsOn?: number events: TEvent[] viewMode: 'month' | 'week' | number - locale?: string + locale?: Parameters['0'] onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void } From e54293462cadfd4fa09d65b294d85e69f12b2ec7 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 4 Jun 2024 22:37:59 +0200 Subject: [PATCH 19/95] refactor: useCalendar reducer --- docs/framework/react/reference/uesCalendar.md | 24 ++--- .../react-time/src/tests/useCalendar.test.ts | 10 +- .../src/useCalendar/calendarActions.ts | 10 +- .../react-time/src/useCalendar/useCalendar.ts | 100 ++++-------------- .../src/useCalendar/useCalendarReducer.ts | 94 +++++++++++++--- .../src/useCalendar/useCalendarState.ts | 1 + packages/time/package.json | 5 +- packages/time/src/index.ts | 2 +- packages/time/src/utils/getFirstDayOfMonth.ts | 4 + packages/time/src/utils/getFirstDayOfWeek.ts | 6 ++ packages/time/src/utils/index.ts | 3 + 11 files changed, 147 insertions(+), 112 deletions(-) create mode 100644 packages/time/src/utils/getFirstDayOfMonth.ts create mode 100644 packages/time/src/utils/getFirstDayOfWeek.ts create mode 100644 packages/time/src/utils/index.ts diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/uesCalendar.md index 3ac395a..7bf0a04 100644 --- a/docs/framework/react/reference/uesCalendar.md +++ b/docs/framework/react/reference/uesCalendar.md @@ -40,11 +40,11 @@ export function useCalendar({ - This value represents the first day of the current period displayed by the calendar. - `currPeriod: string` - This value represents a string that describes the current period displayed by the calendar. -- `getPrev: MouseEventHandler` +- `setPreviousPeriod: MouseEventHandler` - This function is a click event handler that navigates to the previous period. -- `getNext: MouseEventHandler` +- `setNextPeriod: MouseEventHandler` - This function is a click event handler that navigates to the next period. -- `getCurrent: MouseEventHandler` +- `getCurrentPeriod: MouseEventHandler` - This function is a click event handler that navigates to the current period. - `get: (date: Temporal.PlainDate) => void` - This function is a callback function that is called when a date is selected in the calendar. It receives the selected date as an argument. @@ -62,7 +62,7 @@ export function useCalendar({ - This function is used to retrieve the style properties for a specific event based on its ID. - `getEventProps: (id: string) => { style: CSSProperties } | null` - This function is used to retrieve the style properties for a specific event based on its ID. -- `getCurrentTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` +- `getCurrentPeriodTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` - This function is used to retrieve the style properties and current time for the current time marker. @@ -73,16 +73,16 @@ const CalendarComponent = ({ events }) => { const { firstDayOfPeriod, currPeriod, - getPrev, - getNext, - getCurrent, + setPreviousPeriod, + setNextPeriod, + getCurrentPeriod, get, changeViewMode, chunks, daysNames, viewMode, getEventProps, - getCurrentTimeMarkerProps, + getCurrentPeriodTimeMarkerProps, } = useCalendar({ events, viewMode: 'month', @@ -93,9 +93,9 @@ const CalendarComponent = ({ events }) => { return (
- - - + + +
@@ -136,7 +136,7 @@ const CalendarComponent = ({ events }) => {
))} -
+
))} diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.ts index a425434..a66a0b7 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.ts @@ -37,7 +37,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.getPrev(mockEvent) + result.current.setPreviousPeriod(mockEvent) }) const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ @@ -60,7 +60,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.getNext(mockEvent) + result.current.setNextPeriod(mockEvent) }) const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) @@ -79,8 +79,8 @@ describe('useCalendar', () => { ) act(() => { - result.current.getNext(mockEvent) - result.current.getCurrent(mockEvent) + result.current.setNextPeriod(mockEvent) + result.current.getCurrentPeriod(mockEvent) }) expect(result.current.currPeriod).toBe( @@ -183,7 +183,7 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: 'week' }), ) - const currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps() + const currentTimeMarkerProps = result.current.getCurrentPeriodTimeMarkerProps() expect(currentTimeMarkerProps).toEqual({ style: { diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts index c713b37..c9190d5 100644 --- a/packages/react-time/src/useCalendar/calendarActions.ts +++ b/packages/react-time/src/useCalendar/calendarActions.ts @@ -2,9 +2,11 @@ import { createAction } from 'typesafe-actions'; import type { Temporal } from '@js-temporal/polyfill'; import type { ActionType } from 'typesafe-actions'; -export const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); -export const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); -export const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); +const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); +const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); +const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); +const setNextPeriodPeriod = createAction('SET_NEXT_PERIOD') +const setPreviousPeriod = createAction('SET_PREVIOUS_PERIOD') -const actions = { setCurrentPeriod, setViewMode, updateCurrentTime }; +export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, setNextPeriodPeriod, setPreviousPeriod }; export type CalendarAction = ActionType; diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 89ae6b8..5b31309 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,10 +1,7 @@ import { useCallback, useEffect, useMemo } from 'react' import { Temporal } from '@js-temporal/polyfill' -import { - setCurrentPeriod, - setViewMode, - updateCurrentTime, -} from './calendarActions' +import { getFirstDayOfMonth, getFirstDayOfWeek } from "@tanstack/time"; +import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' import type { Event } from './useCalendarState' import type { CSSProperties, MouseEventHandler } from 'react' @@ -17,14 +14,6 @@ interface UseCalendarProps { onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void } -const getFirstDayOfMonth = (currMonth: string) => - Temporal.PlainDate.from(`${currMonth}-01`) - -const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { - const date = Temporal.PlainDate.from(currWeek) - return date.subtract({ days: (date.dayOfWeek - weekStartsOn + 7) % 7 }) -} - const getChunks = function* (arr: T[], n: number) { for (let i = 0; i < arr.length; i += n) { yield arr.slice(i, i + n) @@ -102,13 +91,16 @@ export const useCalendar = ({ currPeriod: today, viewMode: initialViewMode, currentTime: Temporal.Now.plainDateTimeISO(), + weekStartsOn, }) + const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ) + const firstDayOfWeek = getFirstDayOfWeek( state.currPeriod.toString(), - weekStartsOn, + state.weekStartsOn, ) const days = @@ -186,67 +178,21 @@ export const useCalendar = ({ }) }) - const getPrev = useCallback>(() => { - switch (state.viewMode) { - case 'month': { - const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: 1 }) - dispatch(setCurrentPeriod(firstDayOfPrevMonth)) - break - } - case 'week': { - const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: 1 }) - dispatch(setCurrentPeriod(firstDayOfPrevWeek)) - break - } - default: { - const prevCustomStart = state.currPeriod.subtract({ - days: state.viewMode, - }) - dispatch(setCurrentPeriod(prevCustomStart)) - break - } - } - }, [ - state.viewMode, - state.currPeriod, - firstDayOfMonth, - dispatch, - firstDayOfWeek, - ]) - - const getNext = useCallback>(() => { - switch (state.viewMode) { - case 'month': { - const firstDayOfNextMonth = firstDayOfMonth.add({ months: 1 }) - dispatch(setCurrentPeriod(firstDayOfNextMonth)) - break - } - case 'week': { - const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: 1 }) - dispatch(setCurrentPeriod(firstDayOfNextWeek)) - break - } - default: { - const nextCustomStart = state.currPeriod.add({ days: state.viewMode }) - dispatch(setCurrentPeriod(nextCustomStart)) - break - } - } - }, [ - state.viewMode, - state.currPeriod, - firstDayOfMonth, - dispatch, - firstDayOfWeek, - ]) - - const getCurrent = useCallback>(() => { - dispatch(setCurrentPeriod(Temporal.Now.plainDateISO())) + const setPreviousPeriod = useCallback>(() => { + dispatch(actions.setPreviousPeriod) + }, [dispatch]) + + const setNextPeriod = useCallback>(() => { + dispatch(actions.setNextPeriodPeriod) + }, [dispatch]) + + const getCurrentPeriod = useCallback>(() => { + dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())) }, [dispatch]) const get = useCallback( (date: Temporal.PlainDate) => { - dispatch(setCurrentPeriod(date)) + dispatch(actions.setCurrentPeriod(date)) }, [dispatch], ) @@ -258,8 +204,8 @@ export const useCalendar = ({ const changeViewMode = useCallback( (newViewMode: 'month' | 'week' | number) => { + dispatch(actions.setViewMode(newViewMode)) onChangeViewMode?.(newViewMode) - dispatch(setViewMode(newViewMode)) }, [dispatch, onChangeViewMode], ) @@ -359,7 +305,7 @@ export const useCalendar = ({ return () => clearInterval(intervalId) }, [dispatch]) - const getCurrentTimeMarkerProps = useCallback(() => { + const getCurrentPeriodTimeMarkerProps = useCallback(() => { const { hour, minute } = state.currentTime const currentTimeInMinutes = hour * 60 + minute const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 @@ -382,9 +328,9 @@ export const useCalendar = ({ ? firstDayOfWeek : state.currPeriod, currPeriod: state.currPeriod.toString({ calendarName: 'auto' }), - getPrev, - getNext, - getCurrent, + setPreviousPeriod, + setNextPeriod, + getCurrentPeriod, get, chunks, daysNames: days @@ -393,6 +339,6 @@ export const useCalendar = ({ viewMode: state.viewMode, changeViewMode, getEventProps, - getCurrentTimeMarkerProps, + getCurrentPeriodTimeMarkerProps, } } diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 06391a4..81efe8f 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -1,29 +1,99 @@ -import { useReducer } from 'react'; -import { createReducer } from 'typesafe-actions'; +import { useReducer } from 'react' +import { createReducer } from 'typesafe-actions' -import { setCurrentPeriod, setViewMode, updateCurrentTime } from './calendarActions'; -import type { CalendarAction} from './calendarActions'; -import type { CalendarState} from './useCalendarState'; +import { getFirstDayOfMonth, getFirstDayOfWeek } from '@tanstack/time' +import { type CalendarAction, actions } from './calendarActions' +import type { CalendarState } from './useCalendarState' const createCalendarReducer = (initialState: CalendarState) => { return createReducer(initialState) - .handleAction(setCurrentPeriod, (state, action) => ({ + .handleAction(actions.setCurrentPeriod, (state, action) => ({ ...state, currPeriod: action.payload, })) - .handleAction(setViewMode, (state, action) => ({ + .handleAction(actions.setViewMode, (state, action) => ({ ...state, viewMode: action.payload, })) - .handleAction(updateCurrentTime, (state, action) => ({ + .handleAction(actions.updateCurrentTime, (state, action) => ({ ...state, currentTime: action.payload, - })); + })) + .handleAction(actions.setPreviousPeriod, (state) => { + const firstDayOfMonth = getFirstDayOfMonth( + state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + ) + const firstDayOfWeek = getFirstDayOfWeek( + state.currPeriod.toString(), + state.weekStartsOn, + ) + + switch (state.viewMode) { + case 'month': { + const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: 1 }) + return { + ...state, + currPeriod: firstDayOfPrevMonth, + } + } + case 'week': { + const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: 1 }) + return { + ...state, + currPeriod: firstDayOfPrevWeek, + } + } + default: { + const prevCustomStart = state.currPeriod.subtract({ + days: state.viewMode, + }) + return { + ...state, + currPeriod: prevCustomStart, + } + } + } + }) + .handleAction(actions.setNextPeriodPeriod, (state, action) => { + const firstDayOfMonth = getFirstDayOfMonth( + state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + ) + const firstDayOfWeek = getFirstDayOfWeek( + state.currPeriod.toString(), + state.weekStartsOn, + ) + + switch (state.viewMode) { + case 'month': { + const firstDayOfNextMonth = firstDayOfMonth.add({ months: 1 }) + return { + ...state, + currPeriod: firstDayOfNextMonth, + } + } + case 'week': { + const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: 1 }) + return { + ...state, + currPeriod: firstDayOfNextWeek, + } + } + default: { + const nextCustomStart = state.currPeriod.add({ days: state.viewMode }) + return { + ...state, + currPeriod: nextCustomStart, + } + } + } + }) } -export const useCalendarReducer = ( +export const useCalendarReducer = < + TState extends CalendarState = CalendarState, +>( initialState: TState, ) => { - const reducer = createCalendarReducer(initialState); - return useReducer(reducer, initialState); + const reducer = createCalendarReducer(initialState) + return useReducer(reducer, initialState) } diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts index 4f7eb91..cd57a88 100644 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -11,4 +11,5 @@ export interface CalendarState { currPeriod: Temporal.PlainDate; viewMode: 'month' | 'week' | number; currentTime: Temporal.PlainDateTime; + weekStartsOn: number; } diff --git a/packages/time/package.json b/packages/time/package.json index 081903b..1b37016 100644 --- a/packages/time/package.json +++ b/packages/time/package.json @@ -55,5 +55,8 @@ "files": [ "dist", "src" - ] + ], + "dependencies": { + "@js-temporal/polyfill": "^0.4.4" + } } diff --git a/packages/time/src/index.ts b/packages/time/src/index.ts index 12981a2..0e36b7b 100644 --- a/packages/time/src/index.ts +++ b/packages/time/src/index.ts @@ -1,4 +1,4 @@ /** * TanStack Time */ -export * from './utils/parse'; \ No newline at end of file +export * from './utils'; \ No newline at end of file diff --git a/packages/time/src/utils/getFirstDayOfMonth.ts b/packages/time/src/utils/getFirstDayOfMonth.ts new file mode 100644 index 0000000..845b49b --- /dev/null +++ b/packages/time/src/utils/getFirstDayOfMonth.ts @@ -0,0 +1,4 @@ +import { Temporal } from '@js-temporal/polyfill' + +export const getFirstDayOfMonth = (currMonth: string) => + Temporal.PlainDate.from(`${currMonth}-01`) diff --git a/packages/time/src/utils/getFirstDayOfWeek.ts b/packages/time/src/utils/getFirstDayOfWeek.ts new file mode 100644 index 0000000..bb02180 --- /dev/null +++ b/packages/time/src/utils/getFirstDayOfWeek.ts @@ -0,0 +1,6 @@ +import { Temporal } from '@js-temporal/polyfill' + +export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { + const date = Temporal.PlainDate.from(currWeek) + return date.subtract({ days: (date.dayOfWeek - weekStartsOn + 7) % 7 }) +} diff --git a/packages/time/src/utils/index.ts b/packages/time/src/utils/index.ts new file mode 100644 index 0000000..293009a --- /dev/null +++ b/packages/time/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './parse'; +export * from './getFirstDayOfMonth'; +export * from './getFirstDayOfWeek'; From a1497781695311ecff4074f8c397120e9cc3179f Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 11:05:56 +0200 Subject: [PATCH 20/95] refactor: useCalendar reducer --- packages/react-time/src/useCalendar/calendarActions.ts | 8 ++++---- packages/react-time/src/useCalendar/useCalendar.ts | 6 +++--- packages/react-time/src/useCalendar/useCalendarReducer.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts index c9190d5..ddccef7 100644 --- a/packages/react-time/src/useCalendar/calendarActions.ts +++ b/packages/react-time/src/useCalendar/calendarActions.ts @@ -5,8 +5,8 @@ import type { ActionType } from 'typesafe-actions'; const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); -const setNextPeriodPeriod = createAction('SET_NEXT_PERIOD') -const setPreviousPeriod = createAction('SET_PREVIOUS_PERIOD') +const setNextPeriod = createAction('SET_NEXT_PERIOD')(); +const setPreviousPeriod = createAction('SET_PREVIOUS_PERIOD')(); -export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, setNextPeriodPeriod, setPreviousPeriod }; -export type CalendarAction = ActionType; +export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, setNextPeriod, setPreviousPeriod }; +export type UseCalendarAction = ActionType; diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 5b31309..e30691d 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -179,11 +179,11 @@ export const useCalendar = ({ }) const setPreviousPeriod = useCallback>(() => { - dispatch(actions.setPreviousPeriod) + dispatch(actions.setPreviousPeriod()) }, [dispatch]) const setNextPeriod = useCallback>(() => { - dispatch(actions.setNextPeriodPeriod) + dispatch(actions.setNextPeriod()); }, [dispatch]) const getCurrentPeriod = useCallback>(() => { @@ -299,7 +299,7 @@ export const useCalendar = ({ useEffect(() => { const intervalId = setInterval(() => { - dispatch(updateCurrentTime(Temporal.Now.plainDateTimeISO())) + dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())) }, 60000) return () => clearInterval(intervalId) diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 81efe8f..57fde84 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -2,11 +2,11 @@ import { useReducer } from 'react' import { createReducer } from 'typesafe-actions' import { getFirstDayOfMonth, getFirstDayOfWeek } from '@tanstack/time' -import { type CalendarAction, actions } from './calendarActions' +import { type UseCalendarAction, actions } from './calendarActions' import type { CalendarState } from './useCalendarState' const createCalendarReducer = (initialState: CalendarState) => { - return createReducer(initialState) + return createReducer(initialState) .handleAction(actions.setCurrentPeriod, (state, action) => ({ ...state, currPeriod: action.payload, @@ -54,7 +54,7 @@ const createCalendarReducer = (initialState: CalendarState) => { } } }) - .handleAction(actions.setNextPeriodPeriod, (state, action) => { + .handleAction(actions.setNextPeriod, (state) => { const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ) From 28ac64f8e1ba30f6a83e14c67d5e38d8ce38bf27 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 11:06:07 +0200 Subject: [PATCH 21/95] refactor: useCalendar reducer --- pnpm-lock.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8df55dd..6e5ac1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,7 +189,11 @@ importers: specifier: ^2.10.1 version: 2.10.1(@testing-library/jest-dom@6.4.2)(solid-js@1.7.8)(vite@5.2.6) - packages/time: {} + packages/time: + dependencies: + '@js-temporal/polyfill': + specifier: ^0.4.4 + version: 0.4.4 packages/vue-time: dependencies: From dfa353a60eef68340366a8faa288331f1ebf2e63 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 12:16:24 +0200 Subject: [PATCH 22/95] feat: add the isToday field --- packages/react-time/src/useCalendar/useCalendar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index e30691d..5244528 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -174,6 +174,7 @@ export const useCalendar = ({ return { date: day, events: dailyEvents, + isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, } }) }) From 874208741db48acf61e4ef6075cb557b43f1744f Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 12:57:28 +0200 Subject: [PATCH 23/95] feat: add the isToday field --- ...eCalendar.test.ts => useCalendar.test.tsx} | 16 +++++++ .../react-time/src/useCalendar/useCalendar.ts | 44 +++++++++---------- 2 files changed, 37 insertions(+), 23 deletions(-) rename packages/react-time/src/tests/{useCalendar.test.ts => useCalendar.test.tsx} (91%) diff --git a/packages/react-time/src/tests/useCalendar.test.ts b/packages/react-time/src/tests/useCalendar.test.tsx similarity index 91% rename from packages/react-time/src/tests/useCalendar.test.ts rename to packages/react-time/src/tests/useCalendar.test.tsx index a66a0b7..0603e90 100644 --- a/packages/react-time/src/tests/useCalendar.test.ts +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -196,4 +196,20 @@ describe('useCalendar', () => { vi.useRealTimers(); }) + + test('should render array of days', () => { + vi.setSystemTime(new Date('2024-06-01T11:00:00')); + + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + ); + + const { chunks } = result.current; + expect(chunks).toHaveLength(5); + expect(chunks[0]).toHaveLength(7); + + expect(chunks[0]?.[0]?.date.toString()).toBe('2024-06-01'); + expect(chunks[chunks.length - 1]?.[0]?.date.toString()).toBe('2024-06-29'); + expect(chunks[0]?.[0]?.isToday).toBe(true); + }); }) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 5244528..7990026 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -8,7 +8,7 @@ import type { CSSProperties, MouseEventHandler } from 'react' interface UseCalendarProps { weekStartsOn?: number - events: TEvent[] + events?: TEvent[] viewMode: 'month' | 'week' | number locale?: Parameters['0'] onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void @@ -113,7 +113,7 @@ export const useCalendar = ({ ), 7, ), - ) + ).flat() : state.viewMode === 'week' ? Array.from( getChunks( @@ -123,7 +123,7 @@ export const useCalendar = ({ ), 7, ), - ) + ).flat() : Array.from( getChunks( generateDateRange( @@ -132,12 +132,12 @@ export const useCalendar = ({ ), state.viewMode, ), - ) + ).flat() const eventMap = useMemo(() => { const map = new Map() - events.forEach((event) => { + events?.forEach((event) => { const eventStartDate = Temporal.PlainDateTime.from(event.startDate) const eventEndDate = Temporal.PlainDateTime.from(event.endDate) if ( @@ -166,19 +166,22 @@ export const useCalendar = ({ return map }, [events]) - const daysWithEvents = days.map((dayChunk) => { - return dayChunk.map((day) => { - const dayKey = day.toString() - const dailyEvents = eventMap.get(dayKey) ?? [] + const daysWithEvents = days.map((day) => { + const dayKey = day.toString() + const dailyEvents = eventMap.get(dayKey) ?? [] - return { - date: day, - events: dailyEvents, - isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, - } - }) + return { + date: day, + events: dailyEvents, + isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, + } }) + const chunks = + state.viewMode === 'month' + ? [...getChunks(daysWithEvents, 7)] + : [daysWithEvents] + const setPreviousPeriod = useCallback>(() => { dispatch(actions.setPreviousPeriod()) }, [dispatch]) @@ -198,11 +201,6 @@ export const useCalendar = ({ [dispatch], ) - const chunks = - state.viewMode === 'month' - ? [...getChunks(daysWithEvents, 7)] - : [daysWithEvents] - const changeViewMode = useCallback( (newViewMode: 'month' | 'week' | number) => { dispatch(actions.setViewMode(newViewMode)) @@ -334,9 +332,9 @@ export const useCalendar = ({ getCurrentPeriod, get, chunks, - daysNames: days - .flat() - .map((day) => day.toLocaleString(locale, { weekday: 'short' })), + daysNames: Array.from(getChunks(daysWithEvents, 7)).flat() + .slice(0, 7) + .map((day) => day.date.toLocaleString(locale, { weekday: 'short' })), viewMode: state.viewMode, changeViewMode, getEventProps, From 3d6f74a8b75eac107ab44a3299653384fa81441a Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 12:58:46 +0200 Subject: [PATCH 24/95] feat: add the isToday field --- packages/react-time/src/tests/useCalendar.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 0603e90..c390cb8 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -210,6 +210,6 @@ describe('useCalendar', () => { expect(chunks[0]?.[0]?.date.toString()).toBe('2024-06-01'); expect(chunks[chunks.length - 1]?.[0]?.date.toString()).toBe('2024-06-29'); - expect(chunks[0]?.[0]?.isToday).toBe(true); + expect(chunks.find((week) => week.some((day) => day.isToday))?.find((day) => day.isToday)?.date.toString()).toBe('2024-06-01'); }); }) From d93b29d0d56432ff694ff4d7f6cc8bf37cc284c9 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 13:09:58 +0200 Subject: [PATCH 25/95] docs: useCalendar --- .../react/reference/{uesCalendar.md => useCalendar.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/framework/react/reference/{uesCalendar.md => useCalendar.md} (99%) diff --git a/docs/framework/react/reference/uesCalendar.md b/docs/framework/react/reference/useCalendar.md similarity index 99% rename from docs/framework/react/reference/uesCalendar.md rename to docs/framework/react/reference/useCalendar.md index 7bf0a04..2d8fea7 100644 --- a/docs/framework/react/reference/uesCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -136,7 +136,7 @@ const CalendarComponent = ({ events }) => {
))} -
+
))} From 6432973e85d90a4eb1d0485b17cae1b8e1cf2037 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 5 Jun 2024 23:48:55 +0200 Subject: [PATCH 26/95] feat: useCalendar hook --- docs/framework/react/reference/useCalendar.md | 34 +++++++++---------- .../react-time/src/tests/useCalendar.test.tsx | 22 ++++++------ .../src/useCalendar/calendarActions.ts | 6 ++-- .../react-time/src/useCalendar/useCalendar.ts | 28 +++++++-------- .../src/useCalendar/useCalendarReducer.ts | 4 +-- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 2d8fea7..dcb86e9 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -40,15 +40,15 @@ export function useCalendar({ - This value represents the first day of the current period displayed by the calendar. - `currPeriod: string` - This value represents a string that describes the current period displayed by the calendar. -- `setPreviousPeriod: MouseEventHandler` +- `goToPreviousPeriod: MouseEventHandler` - This function is a click event handler that navigates to the previous period. -- `setNextPeriod: MouseEventHandler` +- `goToNextPeriod: MouseEventHandler` - This function is a click event handler that navigates to the next period. -- `getCurrentPeriod: MouseEventHandler` +- `goToCurrentPeriod: MouseEventHandler` - This function is a click event handler that navigates to the current period. -- `get: (date: Temporal.PlainDate) => void` +- `goToSpecificPeriod: (date: Temporal.PlainDate) => void` - This function is a callback function that is called when a date is selected in the calendar. It receives the selected date as an argument. -- `chunks: Array>` +- `weeks: Array>` - This value represents the calendar grid, where each cell contains the date and events for that day. - `daysNames: string[]` - This value represents an array of strings that contain the names of the days of the week. @@ -62,7 +62,7 @@ export function useCalendar({ - This function is used to retrieve the style properties for a specific event based on its ID. - `getEventProps: (id: string) => { style: CSSProperties } | null` - This function is used to retrieve the style properties for a specific event based on its ID. -- `getCurrentPeriodTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` +- `goToCurrentPeriodTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` - This function is used to retrieve the style properties and current time for the current time marker. @@ -73,16 +73,16 @@ const CalendarComponent = ({ events }) => { const { firstDayOfPeriod, currPeriod, - setPreviousPeriod, - setNextPeriod, - getCurrentPeriod, - get, + goToPreviousPeriod, + goToNextPeriod, + goToCurrentPeriod, + goToSpecificPeriod, changeViewMode, - chunks, + weeks, daysNames, viewMode, getEventProps, - getCurrentPeriodTimeMarkerProps, + goToCurrentPeriodTimeMarkerProps, } = useCalendar({ events, viewMode: 'month', @@ -93,9 +93,9 @@ const CalendarComponent = ({ events }) => { return (
- - - + + +
@@ -116,7 +116,7 @@ const CalendarComponent = ({ events }) => { )} - {chunks.map((week, weekIndex) => ( + {weeks.map((week, weekIndex) => ( {week.map((day) => ( @@ -136,7 +136,7 @@ const CalendarComponent = ({ events }) => {
))} -
+
))} diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index c390cb8..9a73267 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -37,7 +37,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.setPreviousPeriod(mockEvent) + result.current.goToPreviousPeriod(mockEvent) }) const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ @@ -60,7 +60,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.setNextPeriod(mockEvent) + result.current.goToNextPeriod(mockEvent) }) const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) @@ -79,8 +79,8 @@ describe('useCalendar', () => { ) act(() => { - result.current.setNextPeriod(mockEvent) - result.current.getCurrentPeriod(mockEvent) + result.current.goToNextPeriod(mockEvent) + result.current.goToCurrentPeriod(mockEvent) }) expect(result.current.currPeriod).toBe( @@ -183,7 +183,7 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: 'week' }), ) - const currentTimeMarkerProps = result.current.getCurrentPeriodTimeMarkerProps() + const currentTimeMarkerProps = result.current.goToCurrentPeriodTimeMarkerProps() expect(currentTimeMarkerProps).toEqual({ style: { @@ -204,12 +204,12 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: 'month', locale: 'en-US' }) ); - const { chunks } = result.current; - expect(chunks).toHaveLength(5); - expect(chunks[0]).toHaveLength(7); + const { weeks } = result.current; + expect(weeks).toHaveLength(5); + expect(weeks[0]).toHaveLength(7); - expect(chunks[0]?.[0]?.date.toString()).toBe('2024-06-01'); - expect(chunks[chunks.length - 1]?.[0]?.date.toString()).toBe('2024-06-29'); - expect(chunks.find((week) => week.some((day) => day.isToday))?.find((day) => day.isToday)?.date.toString()).toBe('2024-06-01'); + expect(weeks[0]?.[0]?.date.toString()).toBe('2024-06-01'); + expect(weeks[weeks.length - 1]?.[0]?.date.toString()).toBe('2024-06-29'); + expect(weeks.find((week) => week.some((day) => day.isToday))?.find((day) => day.isToday)?.date.toString()).toBe('2024-06-01'); }); }) diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts index ddccef7..aee447f 100644 --- a/packages/react-time/src/useCalendar/calendarActions.ts +++ b/packages/react-time/src/useCalendar/calendarActions.ts @@ -5,8 +5,8 @@ import type { ActionType } from 'typesafe-actions'; const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); -const setNextPeriod = createAction('SET_NEXT_PERIOD')(); -const setPreviousPeriod = createAction('SET_PREVIOUS_PERIOD')(); +const goToNextPeriod = createAction('SET_NEXT_PERIOD')(); +const goToPreviousPeriod = createAction('SET_PREVIOUS_PERIOD')(); -export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, setNextPeriod, setPreviousPeriod }; +export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, goToNextPeriod, goToPreviousPeriod }; export type UseCalendarAction = ActionType; diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 7990026..f52bbd4 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -177,24 +177,24 @@ export const useCalendar = ({ } }) - const chunks = + const weeks = state.viewMode === 'month' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents] - const setPreviousPeriod = useCallback>(() => { - dispatch(actions.setPreviousPeriod()) + const goToPreviousPeriod = useCallback>(() => { + dispatch(actions.goToPreviousPeriod()) }, [dispatch]) - const setNextPeriod = useCallback>(() => { - dispatch(actions.setNextPeriod()); + const goToNextPeriod = useCallback>(() => { + dispatch(actions.goToNextPeriod()); }, [dispatch]) - const getCurrentPeriod = useCallback>(() => { + const goToCurrentPeriod = useCallback>(() => { dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())) }, [dispatch]) - const get = useCallback( + const goToSpecificPeriod = useCallback( (date: Temporal.PlainDate) => { dispatch(actions.setCurrentPeriod(date)) }, @@ -304,7 +304,7 @@ export const useCalendar = ({ return () => clearInterval(intervalId) }, [dispatch]) - const getCurrentPeriodTimeMarkerProps = useCallback(() => { + const goToCurrentPeriodTimeMarkerProps = useCallback(() => { const { hour, minute } = state.currentTime const currentTimeInMinutes = hour * 60 + minute const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 @@ -327,17 +327,17 @@ export const useCalendar = ({ ? firstDayOfWeek : state.currPeriod, currPeriod: state.currPeriod.toString({ calendarName: 'auto' }), - setPreviousPeriod, - setNextPeriod, - getCurrentPeriod, - get, - chunks, + goToPreviousPeriod, + goToNextPeriod, + goToCurrentPeriod, + goToSpecificPeriod, + weeks, daysNames: Array.from(getChunks(daysWithEvents, 7)).flat() .slice(0, 7) .map((day) => day.date.toLocaleString(locale, { weekday: 'short' })), viewMode: state.viewMode, changeViewMode, getEventProps, - getCurrentPeriodTimeMarkerProps, + goToCurrentPeriodTimeMarkerProps, } } diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 57fde84..5aa28d2 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -19,7 +19,7 @@ const createCalendarReducer = (initialState: CalendarState) => { ...state, currentTime: action.payload, })) - .handleAction(actions.setPreviousPeriod, (state) => { + .handleAction(actions.goToPreviousPeriod, (state) => { const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ) @@ -54,7 +54,7 @@ const createCalendarReducer = (initialState: CalendarState) => { } } }) - .handleAction(actions.setNextPeriod, (state) => { + .handleAction(actions.goToNextPeriod, (state) => { const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ) From 75a445a6ec0531ed4289c0d80c8268018fe2b916 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 00:00:01 +0200 Subject: [PATCH 27/95] cd --- .../react-time/src/tests/useCalendar.test.tsx | 24 +++++++++++++++---- .../react-time/src/useCalendar/useCalendar.ts | 23 ++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 9a73267..1657d40 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -106,7 +106,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.get(Temporal.PlainDate.from('2024-06-01')) + result.current.goToSpecificPeriod(Temporal.PlainDate.from('2024-06-01')) }) expect(result.current.currPeriod).toBe('2024-06-01') @@ -208,8 +208,24 @@ describe('useCalendar', () => { expect(weeks).toHaveLength(5); expect(weeks[0]).toHaveLength(7); - expect(weeks[0]?.[0]?.date.toString()).toBe('2024-06-01'); - expect(weeks[weeks.length - 1]?.[0]?.date.toString()).toBe('2024-06-29'); + expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-27'); + expect(weeks[weeks.length - 1]?.[0]?.date.toString()).toBe('2024-06-24'); expect(weeks.find((week) => week.some((day) => day.isToday))?.find((day) => day.isToday)?.date.toString()).toBe('2024-06-01'); }); -}) + + test('should return the correct day names based on weekStartsOn', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US', weekStartsOn: 1 }) + ); + + const { daysNames } = result.current; + expect(daysNames).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); + + const { result: resultSundayStart } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US', weekStartsOn: 7 }) + ); + + const { daysNames: sundayDaysNames } = resultSundayStart.current; + expect(sundayDaysNames).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + }); +}); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index f52bbd4..6a80b1f 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,11 +1,18 @@ import { useCallback, useEffect, useMemo } from 'react' import { Temporal } from '@js-temporal/polyfill' -import { getFirstDayOfMonth, getFirstDayOfWeek } from "@tanstack/time"; import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' import type { Event } from './useCalendarState' import type { CSSProperties, MouseEventHandler } from 'react' +export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { + const date = Temporal.PlainDate.from(currWeek) + return date.subtract({ days: (date.dayOfWeek - weekStartsOn + 7) % 7 }) +} + +export const getFirstDayOfMonth = (currMonth: string) => + Temporal.PlainDate.from(`${currMonth}-01`) + interface UseCalendarProps { weekStartsOn?: number events?: TEvent[] @@ -108,7 +115,7 @@ export const useCalendar = ({ ? Array.from( getChunks( generateDateRange( - firstDayOfMonth, + firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - state.weekStartsOn + 7) % 7 }), firstDayOfMonth.add({ months: 1 }).subtract({ days: 1 }), ), 7, @@ -319,6 +326,14 @@ export const useCalendar = ({ } }, [state.currentTime]) + const daysNames = useMemo(() => { + const baseDate = Temporal.PlainDate.from('2024-01-01') + return Array.from({ length: 7 }).map((_, i) => + baseDate.add({ days: (i + weekStartsOn - 1) % 7 }) + .toLocaleString(locale, { weekday: 'short' }) + ) + }, [locale, weekStartsOn]) + return { firstDayOfPeriod: state.viewMode === 'month' @@ -332,9 +347,7 @@ export const useCalendar = ({ goToCurrentPeriod, goToSpecificPeriod, weeks, - daysNames: Array.from(getChunks(daysWithEvents, 7)).flat() - .slice(0, 7) - .map((day) => day.date.toLocaleString(locale, { weekday: 'short' })), + daysNames, viewMode: state.viewMode, changeViewMode, getEventProps, From 7536efc4c6eefc645a666ed052c4b4597743c8a0 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 00:02:08 +0200 Subject: [PATCH 28/95] feat: useCalendar hook --- packages/react-time/src/tests/useCalendar.test.tsx | 2 +- packages/react-time/src/useCalendar/useCalendar.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 1657d40..5321e76 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -183,7 +183,7 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: 'week' }), ) - const currentTimeMarkerProps = result.current.goToCurrentPeriodTimeMarkerProps() + const currentTimeMarkerProps = result.current.currentTimeMarkerProps() expect(currentTimeMarkerProps).toEqual({ style: { diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 6a80b1f..27e4621 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -311,7 +311,7 @@ export const useCalendar = ({ return () => clearInterval(intervalId) }, [dispatch]) - const goToCurrentPeriodTimeMarkerProps = useCallback(() => { + const currentTimeMarkerProps = useCallback(() => { const { hour, minute } = state.currentTime const currentTimeInMinutes = hour * 60 + minute const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 @@ -351,6 +351,6 @@ export const useCalendar = ({ viewMode: state.viewMode, changeViewMode, getEventProps, - goToCurrentPeriodTimeMarkerProps, + currentTimeMarkerProps, } } From 29d20ae54430ce2d8274ab05eff0bcf9f4356f82 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 00:02:18 +0200 Subject: [PATCH 29/95] feat: useCalendar hook --- docs/framework/react/reference/useCalendar.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index dcb86e9..6a1941f 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -62,7 +62,7 @@ export function useCalendar({ - This function is used to retrieve the style properties for a specific event based on its ID. - `getEventProps: (id: string) => { style: CSSProperties } | null` - This function is used to retrieve the style properties for a specific event based on its ID. -- `goToCurrentPeriodTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` +- `currentTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` - This function is used to retrieve the style properties and current time for the current time marker. @@ -82,7 +82,7 @@ const CalendarComponent = ({ events }) => { daysNames, viewMode, getEventProps, - goToCurrentPeriodTimeMarkerProps, + currentTimeMarkerProps, } = useCalendar({ events, viewMode: 'month', @@ -136,7 +136,7 @@ const CalendarComponent = ({ events }) => {
))} -
+
))} From e37dbf7f646282d1c6be4000d8cb5191ed74f766 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 00:14:43 +0200 Subject: [PATCH 30/95] feat: useCalendar hook --- .../react-time/src/tests/useCalendar.test.tsx | 26 ++++++++++++++++--- .../react-time/src/useCalendar/useCalendar.ts | 2 ++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 5321e76..e4926ee 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -1,9 +1,13 @@ import { Temporal } from '@js-temporal/polyfill' -import { describe, expect, test, vi } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useCalendar } from '../useCalendar' describe('useCalendar', () => { + afterEach(() => { + vi.useRealTimers(); + }); + const events = [ { id: '1', @@ -193,8 +197,6 @@ describe('useCalendar', () => { }, currentTime: '11:00', }) - - vi.useRealTimers(); }) test('should render array of days', () => { @@ -228,4 +230,22 @@ describe('useCalendar', () => { const { daysNames: sundayDaysNames } = resultSundayStart.current; expect(sundayDaysNames).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); }); + + test('should correctly mark days as in current period', () => { + vi.setSystemTime(new Date('2024-06-01T11:00:00')); + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + ); + + const { weeks } = result.current; + const daysInCurrentPeriod = weeks.flat().map(day => day.isInCurrentPeriod); + + expect(daysInCurrentPeriod).toEqual([ + false, false, false, false, false, true, true, + true, true, true, true, true, true, true, + true, true, true, true, true, true, true, + true, true, true, true, true, true, true, + true, true, true, true, true, true, true + ]); + }); }); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 27e4621..a882578 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -176,11 +176,13 @@ export const useCalendar = ({ const daysWithEvents = days.map((day) => { const dayKey = day.toString() const dailyEvents = eventMap.get(dayKey) ?? [] + const isInCurrentPeriod = day.month === state.currPeriod.month return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, + isInCurrentPeriod, } }) From d76ddc148110aab895a06004c25157ff5ce11889 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 00:14:51 +0200 Subject: [PATCH 31/95] feat: useCalendar hook --- docs/framework/react/reference/useCalendar.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 6a1941f..a53e07f 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -119,7 +119,11 @@ const CalendarComponent = ({ events }) => { {weeks.map((week, weekIndex) => ( {week.map((day) => ( - +
{day.date.day}
From f6b6f29e9ae1bccc429d06312cdcce0d737e0aa4 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 01:48:24 +0200 Subject: [PATCH 32/95] test: useCalendar --- .../react-time/src/tests/useCalendar.test.tsx | 56 +++++++++++++++++++ .../react-time/src/useCalendar/useCalendar.ts | 8 +-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index e4926ee..ca273c1 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -248,4 +248,60 @@ describe('useCalendar', () => { true, true, true, true, true, true, true ]); }); + + test('should navigate to a specific period correctly', () => { + const { result } = renderHook(() => useCalendar({ events, viewMode: 'month', locale: 'en-US' })) + const specificDate = Temporal.PlainDate.from('2024-05-15') + + act(() => { + result.current.goToSpecificPeriod(specificDate) + }) + + expect(result.current.currPeriod).toEqual(specificDate) + }) + + test('should navigate to the previous period correctly', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + ) + + act(() => { + result.current.goToPreviousPeriod() + }) + + const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ + months: 1, + }) + + expect(result.current.currPeriod).toEqual(expectedPreviousMonth) + }) + + test('should navigate to the next period correctly', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + ) + + act(() => { + result.current.goToNextPeriod() + }) + + const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) + + expect(result.current.currPeriod).toEqual(expectedNextMonth) + }) + + test('should reset to the current period correctly', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + ) + + act(() => { + result.current.goToNextPeriod() + result.current.goToCurrentPeriod() + }) + + expect(result.current.currPeriod).toEqual( + Temporal.Now.plainDateISO(), + ) + }) }); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index a882578..65c7e98 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -191,15 +191,15 @@ export const useCalendar = ({ ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents] - const goToPreviousPeriod = useCallback>(() => { + const goToPreviousPeriod = useCallback(() => { dispatch(actions.goToPreviousPeriod()) }, [dispatch]) - const goToNextPeriod = useCallback>(() => { + const goToNextPeriod = useCallback(() => { dispatch(actions.goToNextPeriod()); }, [dispatch]) - const goToCurrentPeriod = useCallback>(() => { + const goToCurrentPeriod = useCallback(() => { dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())) }, [dispatch]) @@ -337,13 +337,13 @@ export const useCalendar = ({ }, [locale, weekStartsOn]) return { + ...state, firstDayOfPeriod: state.viewMode === 'month' ? firstDayOfMonth : state.viewMode === 'week' ? firstDayOfWeek : state.currPeriod, - currPeriod: state.currPeriod.toString({ calendarName: 'auto' }), goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, From d9ed5e750e0270be3585aef7cf5bcef64e7ca77b Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 01:49:16 +0200 Subject: [PATCH 33/95] test: useCalendar --- .../react-time/src/tests/useCalendar.test.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index ca273c1..0933e15 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -77,21 +77,6 @@ describe('useCalendar', () => { expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfNextMonth) }) - test('should reset to the current period correctly', () => { - const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month' }), - ) - - act(() => { - result.current.goToNextPeriod(mockEvent) - result.current.goToCurrentPeriod(mockEvent) - }) - - expect(result.current.currPeriod).toBe( - Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }), - ) - }) - test('should change view mode correctly', () => { const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' }), From 7b8662b64b6531ed6ccf5ba1607f563474d87cb9 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 01:50:00 +0200 Subject: [PATCH 34/95] test: useCalendar --- packages/react-time/src/tests/useCalendar.test.tsx | 6 ++---- packages/react-time/src/useCalendar/useCalendar.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 0933e15..65a580b 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -23,8 +23,6 @@ describe('useCalendar', () => { }, ] - const mockEvent = {} as React.MouseEvent - test('should initialize with the correct view mode and current period', () => { const { result } = renderHook(() => useCalendar({ events, viewMode: 'month' }), @@ -41,7 +39,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.goToPreviousPeriod(mockEvent) + result.current.goToPreviousPeriod() }) const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ @@ -64,7 +62,7 @@ describe('useCalendar', () => { ) act(() => { - result.current.goToNextPeriod(mockEvent) + result.current.goToNextPeriod() }) const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 65c7e98..9774676 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -3,7 +3,7 @@ import { Temporal } from '@js-temporal/polyfill' import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' import type { Event } from './useCalendarState' -import type { CSSProperties, MouseEventHandler } from 'react' +import type { CSSProperties } from 'react' export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { const date = Temporal.PlainDate.from(currWeek) From be2e360b69d0ddca18c73afafd3af1ff72396553 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 13:34:17 +0200 Subject: [PATCH 35/95] feat: custom reducer --- .../react-time/src/tests/useCalendar.test.tsx | 27 +++++++++++++++++++ .../react-time/src/useCalendar/useCalendar.ts | 9 ++++--- .../src/useCalendar/useCalendarReducer.ts | 13 ++++----- .../src/useCalendar/useCalendarState.ts | 2 +- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 65a580b..ba71454 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -2,6 +2,9 @@ import { Temporal } from '@js-temporal/polyfill' import { afterEach, describe, expect, test, vi } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useCalendar } from '../useCalendar' +import { actions } from '../useCalendar/calendarActions'; +import type { UseCalendarAction} from '../useCalendar/calendarActions'; +import type { UseCalendarState } from '../useCalendar/useCalendarState'; describe('useCalendar', () => { afterEach(() => { @@ -287,4 +290,28 @@ describe('useCalendar', () => { Temporal.Now.plainDateISO(), ) }) + + test(`should allow overriding the reducer`, () => { + const customReducer = (state: UseCalendarState, action: UseCalendarAction) => { + if (action.type === actions.goToNextPeriod().type) { + return { + ...state, + currPeriod: state.currPeriod.add({ months: 2 }), + } + } + + return state + } + + const { result } = renderHook(() => + useCalendar({ events, viewMode: 'month', reducer: customReducer }) + ) + + act(() => { + result.current.goToNextPeriod() + }) + + const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 2 }) + expect(result.current.currPeriod).toEqual(expectedNextMonth) + }); }); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 9774676..a24629a 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo } from 'react' import { Temporal } from '@js-temporal/polyfill' import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' -import type { Event } from './useCalendarState' +import type { UseCalendarAction} from './calendarActions'; +import type { Event, UseCalendarState } from './useCalendarState' import type { CSSProperties } from 'react' export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { @@ -13,12 +14,13 @@ export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { export const getFirstDayOfMonth = (currMonth: string) => Temporal.PlainDate.from(`${currMonth}-01`) -interface UseCalendarProps { +interface UseCalendarProps { weekStartsOn?: number events?: TEvent[] viewMode: 'month' | 'week' | number locale?: Parameters['0'] onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void + reducer?: (state: TState, action: TAction) => TState } const getChunks = function* (arr: T[], n: number) { @@ -92,6 +94,7 @@ export const useCalendar = ({ viewMode: initialViewMode, locale, onChangeViewMode, + reducer, }: UseCalendarProps) => { const today = Temporal.Now.plainDateISO() const [state, dispatch] = useCalendarReducer({ @@ -99,7 +102,7 @@ export const useCalendar = ({ viewMode: initialViewMode, currentTime: Temporal.Now.plainDateTimeISO(), weekStartsOn, - }) + }, reducer) const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 5aa28d2..70a9954 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -1,12 +1,12 @@ -import { useReducer } from 'react' +import { useMemo, useReducer } from 'react' import { createReducer } from 'typesafe-actions' import { getFirstDayOfMonth, getFirstDayOfWeek } from '@tanstack/time' import { type UseCalendarAction, actions } from './calendarActions' -import type { CalendarState } from './useCalendarState' +import type { UseCalendarState } from './useCalendarState' -const createCalendarReducer = (initialState: CalendarState) => { - return createReducer(initialState) +const createCalendarReducer = (initialState: UseCalendarState) => { + return createReducer(initialState) .handleAction(actions.setCurrentPeriod, (state, action) => ({ ...state, currPeriod: action.payload, @@ -90,10 +90,11 @@ const createCalendarReducer = (initialState: CalendarState) => { } export const useCalendarReducer = < - TState extends CalendarState = CalendarState, + TState extends UseCalendarState = UseCalendarState, >( initialState: TState, + extReducer?: (state: TState, action: UseCalendarAction) => TState, ) => { - const reducer = createCalendarReducer(initialState) + const reducer = useMemo(() => extReducer ?? createCalendarReducer(initialState), [extReducer, initialState]) return useReducer(reducer, initialState) } diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts index cd57a88..a2d36e4 100644 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -7,7 +7,7 @@ export interface Event { title: string; } -export interface CalendarState { +export interface UseCalendarState { currPeriod: Temporal.PlainDate; viewMode: 'month' | 'week' | number; currentTime: Temporal.PlainDateTime; From 4ccd56bdf108ad2262a0d7f1c12b0b8575ff0228 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 13:37:37 +0200 Subject: [PATCH 36/95] docs: tsdocs --- .../react-time/src/useCalendar/useCalendar.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index a24629a..30ddfe5 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -15,11 +15,30 @@ export const getFirstDayOfMonth = (currMonth: string) => Temporal.PlainDate.from(`${currMonth}-01`) interface UseCalendarProps { + /** + * The day of the week the calendar should start on (1 for Monday, 7 for Sunday). + * @default 1 + */ weekStartsOn?: number + /** + * An array of events that the calendar should display. + */ events?: TEvent[] + /** + * The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. + */ viewMode: 'month' | 'week' | number + /** + * The locale to use for formatting dates and times. + */ locale?: Parameters['0'] + /** + * Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. + */ onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void + /** + * Custom reducer function to manage the state of the calendar. + */ reducer?: (state: TState, action: TAction) => TState } @@ -88,6 +107,31 @@ const generateDateRange = ( return dates } +/** + * Hook to manage the state and behavior of a calendar. + * + * @param {UseCalendarProps} props - The configuration properties for the calendar. + * @param {number} [props.weekStartsOn=1] - The day of the week the calendar should start on (1 for Monday, 7 for Sunday). + * @param {TEvent[]} [props.events] - An array of events that the calendar should display. + * @param {'month' | 'week' | number} props.viewMode - The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. + * @param {Intl.LocalesArgument} [props.locale] - The locale to use for formatting dates and times. + * @param {Function} [props.onChangeViewMode] - Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. + * @param {Function} [props.reducer] - Custom reducer function to manage the state of the calendar. + * + * @returns {Object} calendarState - The state and functions for managing the calendar. + * @returns {Temporal.PlainDate} calendarState.firstDayOfPeriod - The first day of the current period displayed by the calendar. + * @returns {Temporal.PlainDate} calendarState.currPeriod - The current period displayed by the calendar. + * @returns {Function} calendarState.goToPreviousPeriod - Function to navigate to the previous period. + * @returns {Function} calendarState.goToNextPeriod - Function to navigate to the next period. + * @returns {Function} calendarState.goToCurrentPeriod - Function to navigate to the current period. + * @returns {Function} calendarState.goToSpecificPeriod - Function to navigate to a specific period. + * @returns {Array>} calendarState.weeks - The calendar grid, where each cell contains the date and events for that day. + * @returns {string[]} calendarState.daysNames - An array of day names based on the locale and week start day. + * @returns {'month' | 'week' | number} calendarState.viewMode - The current view mode of the calendar. + * @returns {Function} calendarState.changeViewMode - Function to change the view mode of the calendar. + * @returns {Function} calendarState.getEventProps - Function to retrieve the style properties for a specific event based on its ID. + * @returns {Function} calendarState.currentTimeMarkerProps - Function to retrieve the style properties and current time for the current time marker. + */ export const useCalendar = ({ weekStartsOn = 1, events, From cc59409a7aed4a49060dc05dd8b729eed3d77205 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 13:37:45 +0200 Subject: [PATCH 37/95] docs: tsdocs --- docs/framework/react/reference/useCalendar.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index a53e07f..620e1fe 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -32,6 +32,8 @@ export function useCalendar({ - This parameter is an optional callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. - `onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void` - This parameter is an optional callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. +- `reducer?: (state: CalendarState, action: CalendarAction) => CalendarState` + - This parameter is an optional custom reducer function that can be used to manage the state of the calendar. #### Returns From 8d51b58e3e4a2dde91b817c07bd268a7602027f4 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 13:46:28 +0200 Subject: [PATCH 38/95] docs: tsdocs --- packages/react-time/src/useCalendar/useCalendar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 30ddfe5..e12d456 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -397,7 +397,6 @@ export const useCalendar = ({ goToSpecificPeriod, weeks, daysNames, - viewMode: state.viewMode, changeViewMode, getEventProps, currentTimeMarkerProps, From 1e7168446066e3be01ff0d233cfe4d0df6a4e89e Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 13:59:02 +0200 Subject: [PATCH 39/95] refactor: useCalendar --- .../react-time/src/useCalendar/calendarActions.ts | 4 ++-- packages/react-time/src/useCalendar/useCalendar.ts | 13 ++++++------- .../src/useCalendar/useCalendarReducer.ts | 8 ++++---- .../react-time/src/useCalendar/useCalendarState.ts | 1 - 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts index aee447f..3eb69d4 100644 --- a/packages/react-time/src/useCalendar/calendarActions.ts +++ b/packages/react-time/src/useCalendar/calendarActions.ts @@ -5,8 +5,8 @@ import type { ActionType } from 'typesafe-actions'; const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); -const goToNextPeriod = createAction('SET_NEXT_PERIOD')(); -const goToPreviousPeriod = createAction('SET_PREVIOUS_PERIOD')(); +const goToNextPeriod = createAction('SET_NEXT_PERIOD')<{ weekStartsOn: number }>(); +const goToPreviousPeriod = createAction('SET_PREVIOUS_PERIOD')<{ weekStartsOn: number }>(); export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, goToNextPeriod, goToPreviousPeriod }; export type UseCalendarAction = ActionType; diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index e12d456..e12c438 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -145,7 +145,6 @@ export const useCalendar = ({ currPeriod: today, viewMode: initialViewMode, currentTime: Temporal.Now.plainDateTimeISO(), - weekStartsOn, }, reducer) const firstDayOfMonth = getFirstDayOfMonth( @@ -154,7 +153,7 @@ export const useCalendar = ({ const firstDayOfWeek = getFirstDayOfWeek( state.currPeriod.toString(), - state.weekStartsOn, + weekStartsOn, ) const days = @@ -162,7 +161,7 @@ export const useCalendar = ({ ? Array.from( getChunks( generateDateRange( - firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - state.weekStartsOn + 7) % 7 }), + firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 }), firstDayOfMonth.add({ months: 1 }).subtract({ days: 1 }), ), 7, @@ -239,12 +238,12 @@ export const useCalendar = ({ : [daysWithEvents] const goToPreviousPeriod = useCallback(() => { - dispatch(actions.goToPreviousPeriod()) - }, [dispatch]) + dispatch(actions.goToPreviousPeriod({ weekStartsOn })) + }, [dispatch, weekStartsOn]) const goToNextPeriod = useCallback(() => { - dispatch(actions.goToNextPeriod()); - }, [dispatch]) + dispatch(actions.goToNextPeriod({ weekStartsOn })) + }, [dispatch, weekStartsOn]) const goToCurrentPeriod = useCallback(() => { dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())) diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 70a9954..b8d635b 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -19,13 +19,13 @@ const createCalendarReducer = (initialState: UseCalendarState) => { ...state, currentTime: action.payload, })) - .handleAction(actions.goToPreviousPeriod, (state) => { + .handleAction(actions.goToPreviousPeriod, (state, action) => { const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ) const firstDayOfWeek = getFirstDayOfWeek( state.currPeriod.toString(), - state.weekStartsOn, + action.payload.weekStartsOn, ) switch (state.viewMode) { @@ -54,13 +54,13 @@ const createCalendarReducer = (initialState: UseCalendarState) => { } } }) - .handleAction(actions.goToNextPeriod, (state) => { + .handleAction(actions.goToNextPeriod, (state, action) => { const firstDayOfMonth = getFirstDayOfMonth( state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ) const firstDayOfWeek = getFirstDayOfWeek( state.currPeriod.toString(), - state.weekStartsOn, + action.payload.weekStartsOn, ) switch (state.viewMode) { diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts index a2d36e4..dd3679b 100644 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -11,5 +11,4 @@ export interface UseCalendarState { currPeriod: Temporal.PlainDate; viewMode: 'month' | 'week' | number; currentTime: Temporal.PlainDateTime; - weekStartsOn: number; } From e134065f530bf5330a4ac797b0968f48532d6d95 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 16:40:27 +0200 Subject: [PATCH 40/95] refactor: useCalendar --- .../react-time/src/tests/useCalendar.test.tsx | 9 +- .../react-time/src/useCalendar/useCalendar.ts | 309 ++++++------------ .../src/useCalendar/useCalendarReducer.ts | 26 +- 3 files changed, 111 insertions(+), 233 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index ba71454..4f52462 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -2,7 +2,6 @@ import { Temporal } from '@js-temporal/polyfill' import { afterEach, describe, expect, test, vi } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useCalendar } from '../useCalendar' -import { actions } from '../useCalendar/calendarActions'; import type { UseCalendarAction} from '../useCalendar/calendarActions'; import type { UseCalendarState } from '../useCalendar/useCalendarState'; @@ -31,8 +30,8 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: 'month' }), ) expect(result.current.viewMode).toBe('month') - expect(result.current.currPeriod).toBe( - Temporal.Now.plainDateISO().toString({ calendarName: 'auto' }), + expect(result.current.currPeriod.toString()).toBe( + Temporal.Now.plainDateISO().toString(), ) }) @@ -99,7 +98,7 @@ describe('useCalendar', () => { result.current.goToSpecificPeriod(Temporal.PlainDate.from('2024-06-01')) }) - expect(result.current.currPeriod).toBe('2024-06-01') + expect(result.current.currPeriod.toString()).toBe('2024-06-01') }) test('should return the correct props for an event', () => { @@ -293,7 +292,7 @@ describe('useCalendar', () => { test(`should allow overriding the reducer`, () => { const customReducer = (state: UseCalendarState, action: UseCalendarAction) => { - if (action.type === actions.goToNextPeriod().type) { + if (action.type === 'SET_NEXT_PERIOD') { return { ...state, currPeriod: state.currPeriod.add({ months: 2 }), diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index e12c438..d91929f 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo, useTransition } from 'react' import { Temporal } from '@js-temporal/polyfill' import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' @@ -54,39 +54,14 @@ const splitMultiDayEvents = (event: TEvent): TEvent[] => { const events: TEvent[] = [] let currentDay = startDate - while ( - Temporal.PlainDate.compare( - currentDay.toPlainDate(), - endDate.toPlainDate(), - ) < 0 - ) { - const startOfCurrentDay = currentDay.with({ - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }) - const endOfCurrentDay = currentDay.with({ - hour: 23, - minute: 59, - second: 59, - millisecond: 999, - }) + while (Temporal.PlainDate.compare(currentDay.toPlainDate(), endDate.toPlainDate()) < 0) { + const startOfCurrentDay = currentDay.with({ hour: 0, minute: 0, second: 0, millisecond: 0 }) + const endOfCurrentDay = currentDay.with({ hour: 23, minute: 59, second: 59, millisecond: 999 }) - const eventStart = - Temporal.PlainDateTime.compare(currentDay, startDate) === 0 - ? startDate - : startOfCurrentDay - const eventEnd = - Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 - ? endDate - : endOfCurrentDay - - events.push({ - ...event, - startDate: eventStart, - endDate: eventEnd, - }) + const eventStart = Temporal.PlainDateTime.compare(currentDay, startDate) === 0 ? startDate : startOfCurrentDay + const eventEnd = Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 ? endDate : endOfCurrentDay + + events.push({ ...event, startDate: eventStart, endDate: eventEnd }) currentDay = startOfCurrentDay.add({ days: 1 }) } @@ -94,10 +69,7 @@ const splitMultiDayEvents = (event: TEvent): TEvent[] => { return events } -const generateDateRange = ( - start: Temporal.PlainDate, - end: Temporal.PlainDate, -) => { +const generateDateRange = (start: Temporal.PlainDate, end: Temporal.PlainDate) => { const dates: Temporal.PlainDate[] = [] let current = start while (Temporal.PlainDate.compare(current, end) <= 0) { @@ -146,59 +118,23 @@ export const useCalendar = ({ viewMode: initialViewMode, currentTime: Temporal.Now.plainDateTimeISO(), }, reducer) - - const firstDayOfMonth = getFirstDayOfMonth( - state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), - ) - - const firstDayOfWeek = getFirstDayOfWeek( - state.currPeriod.toString(), - weekStartsOn, - ) - - const days = - state.viewMode === 'month' - ? Array.from( - getChunks( - generateDateRange( - firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 }), - firstDayOfMonth.add({ months: 1 }).subtract({ days: 1 }), - ), - 7, - ), - ).flat() - : state.viewMode === 'week' - ? Array.from( - getChunks( - generateDateRange( - firstDayOfWeek, - firstDayOfWeek.add({ days: 6 }), - ), - 7, - ), - ).flat() - : Array.from( - getChunks( - generateDateRange( - state.currPeriod, - state.currPeriod.add({ days: state.viewMode - 1 }), - ), - state.viewMode, - ), - ).flat() + + const firstDayOfMonth = useMemo(() => getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)), [state.currPeriod]) + + const firstDayOfWeek = useMemo(() => getFirstDayOfWeek(state.currPeriod.toString(), weekStartsOn), [state.currPeriod, weekStartsOn]) + + const days = useMemo(() => { + const start = state.viewMode === 'month' ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 }) : firstDayOfWeek + const end = state.viewMode === 'month' ? firstDayOfMonth.add({ months: 1 }).subtract({ days: 1 }) : firstDayOfWeek.add({ days: 6 }) + return Array.from(getChunks(generateDateRange(start, end), 7)).flat() + }, [state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn]) const eventMap = useMemo(() => { const map = new Map() - events?.forEach((event) => { const eventStartDate = Temporal.PlainDateTime.from(event.startDate) const eventEndDate = Temporal.PlainDateTime.from(event.endDate) - if ( - Temporal.PlainDate.compare( - eventStartDate.toPlainDate(), - eventEndDate.toPlainDate(), - ) !== 0 - ) { + if (Temporal.PlainDate.compare(eventStartDate.toPlainDate(), eventEndDate.toPlainDate()) !== 0) { const splitEvents = splitMultiDayEvents(event) splitEvents.forEach((splitEvent) => { const splitKey = splitEvent.startDate.toString().split('T')[0] @@ -215,147 +151,108 @@ export const useCalendar = ({ } } }) - return map }, [events]) - const daysWithEvents = days.map((day) => { + const daysWithEvents = useMemo(() => days.map((day) => { const dayKey = day.toString() const dailyEvents = eventMap.get(dayKey) ?? [] const isInCurrentPeriod = day.month === state.currPeriod.month + return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, isInCurrentPeriod } + }), [days, eventMap, state.currPeriod]) - return { - date: day, - events: dailyEvents, - isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, - isInCurrentPeriod, - } - }) + const weeks = useMemo(() => state.viewMode === 'month' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents], [state.viewMode, daysWithEvents]) - const weeks = - state.viewMode === 'month' - ? [...getChunks(daysWithEvents, 7)] - : [daysWithEvents] + const [isPending, startTransition] = useTransition() - const goToPreviousPeriod = useCallback(() => { - dispatch(actions.goToPreviousPeriod({ weekStartsOn })) - }, [dispatch, weekStartsOn]) + const goToPreviousPeriod = useCallback(() => startTransition(() => dispatch(actions.goToPreviousPeriod({ weekStartsOn }))), [dispatch, weekStartsOn]) - const goToNextPeriod = useCallback(() => { - dispatch(actions.goToNextPeriod({ weekStartsOn })) - }, [dispatch, weekStartsOn]) + const goToNextPeriod = useCallback(() => startTransition(() => dispatch(actions.goToNextPeriod({ weekStartsOn }))), [dispatch, weekStartsOn]) - const goToCurrentPeriod = useCallback(() => { - dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())) - }, [dispatch]) + const goToCurrentPeriod = useCallback(() => startTransition(() => dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO()))), [dispatch]) - const goToSpecificPeriod = useCallback( - (date: Temporal.PlainDate) => { - dispatch(actions.setCurrentPeriod(date)) - }, - [dispatch], - ) + const goToSpecificPeriod = useCallback((date: Temporal.PlainDate) => startTransition(() => dispatch(actions.setCurrentPeriod(date))), [dispatch]) - const changeViewMode = useCallback( - (newViewMode: 'month' | 'week' | number) => { + const changeViewMode = useCallback((newViewMode: 'month' | 'week' | number) => { + startTransition(() => { dispatch(actions.setViewMode(newViewMode)) onChangeViewMode?.(newViewMode) - }, - [dispatch, onChangeViewMode], - ) + }) + }, [dispatch, onChangeViewMode]) - const getEventProps = useCallback( - (id: Event['id']): { style: CSSProperties } | null => { - const event = [...eventMap.values()] - .flat() - .find((currEvent) => currEvent.id === id) - if (!event) return null + const getEventProps = useCallback((id: Event['id']): { style: CSSProperties } | null => { + const event = [...eventMap.values()].flat().find((currEvent) => currEvent.id === id) + if (!event) return null - const eventStartDate = Temporal.PlainDateTime.from(event.startDate) - const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + const eventStartDate = Temporal.PlainDateTime.from(event.startDate) + const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + const isSplitEvent = Temporal.PlainDate.compare(eventStartDate.toPlainDate(), eventEndDate.toPlainDate()) !== 0 - const isSplitEvent = - Temporal.PlainDate.compare( - eventStartDate.toPlainDate(), - eventEndDate.toPlainDate(), - ) !== 0 - - let percentageOfDay - let eventHeightInMinutes - - if (isSplitEvent) { - const isStartPart = - eventStartDate.hour !== 0 || eventStartDate.minute !== 0 - if (isStartPart) { - const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - eventHeightInMinutes = 24 * 60 - eventTimeInMinutes - } else { - percentageOfDay = 0 - eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute - } - } else { - const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute + let percentageOfDay + let eventHeightInMinutes + + if (isSplitEvent) { + const isStartPart = eventStartDate.hour !== 0 || eventStartDate.minute !== 0 + if (isStartPart) { + const eventTimeInMinutes = eventStartDate.hour * 60 + eventStartDate.minute percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute - eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes + eventHeightInMinutes = 24 * 60 - eventTimeInMinutes + } else { + percentageOfDay = 0 + eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute } + } else { + const eventTimeInMinutes = eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes + } - const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) - - const overlappingEvents = [...eventMap.values()].flat().filter((e) => { - const eStartDate = Temporal.PlainDateTime.from(e.startDate) - const eEndDate = Temporal.PlainDateTime.from(e.endDate) - return ( - (e.id !== id && - Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && - Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && - Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && - Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && - Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) - ) - }) - - const eventIndex = overlappingEvents.findIndex((e) => e.id === id) - const totalOverlaps = overlappingEvents.length - const sidePadding = 2 - const innerPadding = 2 - const totalInnerPadding = (totalOverlaps - 1) * innerPadding - const availableWidth = 100 - totalInnerPadding - 2 * sidePadding - const eventWidth = - totalOverlaps > 0 - ? availableWidth / totalOverlaps - : 100 - 2 * sidePadding - const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - - if (state.viewMode === 'week' || typeof state.viewMode === 'number') { - return { - style: { - position: 'absolute', - top: `min(${percentageOfDay}%, calc(100% - 55px))`, - left: `${eventLeft}%`, - width: `${eventWidth}%`, - margin: 0, - height: `${eventHeight}%`, - }, - } + const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) + + const overlappingEvents = [...eventMap.values()].flat().filter((e) => { + const eStartDate = Temporal.PlainDateTime.from(e.startDate) + const eEndDate = Temporal.PlainDateTime.from(e.endDate) + return ( + (e.id !== id && + Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) + ) + }) + + const eventIndex = overlappingEvents.findIndex((e) => e.id === id) + const totalOverlaps = overlappingEvents.length + const sidePadding = 2 + const innerPadding = 2 + const totalInnerPadding = (totalOverlaps - 1) * innerPadding + const availableWidth = 100 - totalInnerPadding - 2 * sidePadding + const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps : 100 - 2 * sidePadding + const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) + + if (state.viewMode === 'week' || typeof state.viewMode === 'number') { + return { + style: { + position: 'absolute', + top: `min(${percentageOfDay}%, calc(100% - 55px))`, + left: `${eventLeft}%`, + width: `${eventWidth}%`, + margin: 0, + height: `${eventHeight}%`, + }, } + } - return null - }, - [eventMap, state.viewMode], - ) + return null + }, [eventMap, state.viewMode]) useEffect(() => { - const intervalId = setInterval(() => { - dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())) - }, 60000) - + const intervalId = setInterval(() => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), 60000) return () => clearInterval(intervalId) }, [dispatch]) @@ -376,7 +273,7 @@ export const useCalendar = ({ const daysNames = useMemo(() => { const baseDate = Temporal.PlainDate.from('2024-01-01') - return Array.from({ length: 7 }).map((_, i) => + return Array.from({ length: 7 }).map((_, i) => baseDate.add({ days: (i + weekStartsOn - 1) % 7 }) .toLocaleString(locale, { weekday: 'short' }) ) @@ -384,12 +281,7 @@ export const useCalendar = ({ return { ...state, - firstDayOfPeriod: - state.viewMode === 'month' - ? firstDayOfMonth - : state.viewMode === 'week' - ? firstDayOfWeek - : state.currPeriod, + firstDayOfPeriod: state.viewMode === 'month' ? firstDayOfMonth : state.viewMode === 'week' ? firstDayOfWeek : state.currPeriod, goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, @@ -399,5 +291,6 @@ export const useCalendar = ({ changeViewMode, getEventProps, currentTimeMarkerProps, + isPending } -} +} \ No newline at end of file diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index b8d635b..0789b6c 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -20,13 +20,8 @@ const createCalendarReducer = (initialState: UseCalendarState) => { currentTime: action.payload, })) .handleAction(actions.goToPreviousPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth( - state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), - ) - const firstDayOfWeek = getFirstDayOfWeek( - state.currPeriod.toString(), - action.payload.weekStartsOn, - ) + const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)) + const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn) switch (state.viewMode) { case 'month': { @@ -44,9 +39,7 @@ const createCalendarReducer = (initialState: UseCalendarState) => { } } default: { - const prevCustomStart = state.currPeriod.subtract({ - days: state.viewMode, - }) + const prevCustomStart = state.currPeriod.subtract({ days: state.viewMode }) return { ...state, currPeriod: prevCustomStart, @@ -55,13 +48,8 @@ const createCalendarReducer = (initialState: UseCalendarState) => { } }) .handleAction(actions.goToNextPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth( - state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), - ) - const firstDayOfWeek = getFirstDayOfWeek( - state.currPeriod.toString(), - action.payload.weekStartsOn, - ) + const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)) + const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn) switch (state.viewMode) { case 'month': { @@ -89,9 +77,7 @@ const createCalendarReducer = (initialState: UseCalendarState) => { }) } -export const useCalendarReducer = < - TState extends UseCalendarState = UseCalendarState, ->( +export const useCalendarReducer = ( initialState: TState, extReducer?: (state: TState, action: UseCalendarAction) => TState, ) => { From f3f47ed1999b8671c69fb552d10f260c476226f4 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 6 Jun 2024 16:40:34 +0200 Subject: [PATCH 41/95] refactor: useCalendar --- docs/framework/react/reference/useCalendar.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 620e1fe..5454204 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -66,6 +66,8 @@ export function useCalendar({ - This function is used to retrieve the style properties for a specific event based on its ID. - `currentTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` - This function is used to retrieve the style properties and current time for the current time marker. +- `isPending: boolean` + - This value represents whether the calendar is in a pending state. #### Example Usage From 1ee814a0e68ae2ef2cddd07dd051a01e4bb222e6 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 9 Jun 2024 18:08:14 +0200 Subject: [PATCH 42/95] refactor: change viewMode type --- .../react-time/src/tests/useCalendar.test.tsx | 40 ++++++------- .../src/useCalendar/calendarActions.ts | 3 +- .../react-time/src/useCalendar/useCalendar.ts | 37 ++++++------ .../src/useCalendar/useCalendarReducer.ts | 56 ++++++++++--------- .../src/useCalendar/useCalendarState.ts | 19 ++++--- 5 files changed, 82 insertions(+), 73 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 4f52462..bfecedc 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -27,9 +27,9 @@ describe('useCalendar', () => { test('should initialize with the correct view mode and current period', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month' }), + useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), ) - expect(result.current.viewMode).toBe('month') + expect(result.current.viewMode).toEqual({ value: 1, unit: 'months' }) expect(result.current.currPeriod.toString()).toBe( Temporal.Now.plainDateISO().toString(), ) @@ -37,7 +37,7 @@ describe('useCalendar', () => { test('should navigate to the previous period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month' }), + useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), ) act(() => { @@ -60,7 +60,7 @@ describe('useCalendar', () => { test('should navigate to the next period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month' }), + useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), ) act(() => { @@ -79,19 +79,19 @@ describe('useCalendar', () => { test('should change view mode correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month' }), + useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), ) act(() => { - result.current.changeViewMode('week') + result.current.changeViewMode({ value: 1, unit: 'weeks' }) }) - expect(result.current.viewMode).toBe('week') + expect(result.current.viewMode).toEqual({ value: 1, unit: 'weeks' }) }) test('should select a day correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month' }), + useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), ) act(() => { @@ -103,7 +103,7 @@ describe('useCalendar', () => { test('should return the correct props for an event', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'week' }), + useCalendar({ events, viewMode: { value: 1, unit: 'weeks' } }), ) const eventProps = result.current.getEventProps('1') @@ -136,7 +136,7 @@ describe('useCalendar', () => { }, ] const { result } = renderHook(() => - useCalendar({ events: overlappingEvents, viewMode: 'week' }), + useCalendar({ events: overlappingEvents, viewMode: { value: 1, unit: 'weeks' } }), ) const event1Props = result.current.getEventProps('1') @@ -169,7 +169,7 @@ describe('useCalendar', () => { vi.setSystemTime(new Date('2024-06-01T11:00:00')); const { result } = renderHook(() => - useCalendar({ events, viewMode: 'week' }), + useCalendar({ events, viewMode: { value: 1, unit: 'weeks' } }), ) const currentTimeMarkerProps = result.current.currentTimeMarkerProps() @@ -188,7 +188,7 @@ describe('useCalendar', () => { vi.setSystemTime(new Date('2024-06-01T11:00:00')); const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }), ); const { weeks } = result.current; @@ -202,14 +202,14 @@ describe('useCalendar', () => { test('should return the correct day names based on weekStartsOn', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US', weekStartsOn: 1 }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US', weekStartsOn: 1 }) ); const { daysNames } = result.current; expect(daysNames).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); const { result: resultSundayStart } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US', weekStartsOn: 7 }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US', weekStartsOn: 7 }) ); const { daysNames: sundayDaysNames } = resultSundayStart.current; @@ -219,7 +219,7 @@ describe('useCalendar', () => { test('should correctly mark days as in current period', () => { vi.setSystemTime(new Date('2024-06-01T11:00:00')); const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) ); const { weeks } = result.current; @@ -235,7 +235,7 @@ describe('useCalendar', () => { }); test('should navigate to a specific period correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: 'month', locale: 'en-US' })) + const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 1, unit: 'months' } })) const specificDate = Temporal.PlainDate.from('2024-05-15') act(() => { @@ -247,7 +247,7 @@ describe('useCalendar', () => { test('should navigate to the previous period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) ) act(() => { @@ -263,7 +263,7 @@ describe('useCalendar', () => { test('should navigate to the next period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) ) act(() => { @@ -277,7 +277,7 @@ describe('useCalendar', () => { test('should reset to the current period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) ) act(() => { @@ -303,7 +303,7 @@ describe('useCalendar', () => { } const { result } = renderHook(() => - useCalendar({ events, viewMode: 'month', reducer: customReducer }) + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, reducer: customReducer }), ) act(() => { diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts index 3eb69d4..abf5c73 100644 --- a/packages/react-time/src/useCalendar/calendarActions.ts +++ b/packages/react-time/src/useCalendar/calendarActions.ts @@ -1,8 +1,9 @@ import { createAction } from 'typesafe-actions'; import type { Temporal } from '@js-temporal/polyfill'; import type { ActionType } from 'typesafe-actions'; +import type { UseCalendarState } from './useCalendarState'; -const setViewMode = createAction('SET_VIEW_MODE')<'month' | 'week' | number>(); +const setViewMode = createAction('SET_VIEW_MODE')(); const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); const goToNextPeriod = createAction('SET_NEXT_PERIOD')<{ weekStartsOn: number }>(); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index d91929f..9aaef02 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -27,7 +27,7 @@ interface UseCalendarProps void + onChangeViewMode?: (viewMode: UseCalendarState['viewMode']) => void /** * Custom reducer function to manage the state of the calendar. */ @@ -113,6 +113,7 @@ export const useCalendar = ({ reducer, }: UseCalendarProps) => { const today = Temporal.Now.plainDateISO() + const [isPending, startTransition] = useTransition() const [state, dispatch] = useCalendarReducer({ currPeriod: today, viewMode: initialViewMode, @@ -124,10 +125,10 @@ export const useCalendar = ({ const firstDayOfWeek = useMemo(() => getFirstDayOfWeek(state.currPeriod.toString(), weekStartsOn), [state.currPeriod, weekStartsOn]) const days = useMemo(() => { - const start = state.viewMode === 'month' ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 }) : firstDayOfWeek - const end = state.viewMode === 'month' ? firstDayOfMonth.add({ months: 1 }).subtract({ days: 1 }) : firstDayOfWeek.add({ days: 6 }) - return Array.from(getChunks(generateDateRange(start, end), 7)).flat() - }, [state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn]) + const start = state.viewMode.unit === 'months' ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 }) : firstDayOfWeek; + const end = state.viewMode.unit === 'months' ? firstDayOfMonth.add({ months: state.viewMode.value }).subtract({ days: 1 }) : firstDayOfWeek.add({ days: 6 }); + return Array.from(getChunks(generateDateRange(start, end), 7)).flat(); + }, [state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn]); const eventMap = useMemo(() => { const map = new Map() @@ -161,9 +162,7 @@ export const useCalendar = ({ return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, isInCurrentPeriod } }), [days, eventMap, state.currPeriod]) - const weeks = useMemo(() => state.viewMode === 'month' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents], [state.viewMode, daysWithEvents]) - - const [isPending, startTransition] = useTransition() + const weeks = useMemo(() => state.viewMode.unit === 'months' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents], [state.viewMode, daysWithEvents]) const goToPreviousPeriod = useCallback(() => startTransition(() => dispatch(actions.goToPreviousPeriod({ weekStartsOn }))), [dispatch, weekStartsOn]) @@ -173,12 +172,12 @@ export const useCalendar = ({ const goToSpecificPeriod = useCallback((date: Temporal.PlainDate) => startTransition(() => dispatch(actions.setCurrentPeriod(date))), [dispatch]) - const changeViewMode = useCallback((newViewMode: 'month' | 'week' | number) => { + const changeViewMode = useCallback((newViewMode: UseCalendarState['viewMode']) => { startTransition(() => { - dispatch(actions.setViewMode(newViewMode)) - onChangeViewMode?.(newViewMode) - }) - }, [dispatch, onChangeViewMode]) + dispatch(actions.setViewMode(newViewMode)); + onChangeViewMode?.(newViewMode); + }); + }, [dispatch, onChangeViewMode]); const getEventProps = useCallback((id: Event['id']): { style: CSSProperties } | null => { const event = [...eventMap.values()].flat().find((currEvent) => currEvent.id === id) @@ -235,7 +234,9 @@ export const useCalendar = ({ const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps : 100 - 2 * sidePadding const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - if (state.viewMode === 'week' || typeof state.viewMode === 'number') { + console.log('state.viewMode.unit', state.viewMode.unit) + + if (state.viewMode.unit === 'weeks' || state.viewMode.unit === 'days') { return { style: { position: 'absolute', @@ -247,8 +248,8 @@ export const useCalendar = ({ }, } } - - return null + + return null }, [eventMap, state.viewMode]) useEffect(() => { @@ -281,7 +282,7 @@ export const useCalendar = ({ return { ...state, - firstDayOfPeriod: state.viewMode === 'month' ? firstDayOfMonth : state.viewMode === 'week' ? firstDayOfWeek : state.currPeriod, + firstDayOfPeriod: state.viewMode.unit === 'months' ? firstDayOfMonth : state.viewMode.unit === 'weeks' ? firstDayOfWeek : state.currPeriod, goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 0789b6c..793c101 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -20,62 +20,66 @@ const createCalendarReducer = (initialState: UseCalendarState) => { currentTime: action.payload, })) .handleAction(actions.goToPreviousPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)) - const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn) + const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); + const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn); - switch (state.viewMode) { - case 'month': { - const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: 1 }) + switch (state.viewMode.unit) { + case 'months': { + const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: state.viewMode.value }); return { ...state, currPeriod: firstDayOfPrevMonth, - } + }; } - case 'week': { - const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: 1 }) + case 'weeks': { + const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: state.viewMode.value }); return { ...state, currPeriod: firstDayOfPrevWeek, - } + }; } - default: { - const prevCustomStart = state.currPeriod.subtract({ days: state.viewMode }) + case 'days': { + const prevCustomStart = state.currPeriod.subtract({ days: state.viewMode.value }); return { ...state, currPeriod: prevCustomStart, - } + }; } + default: + return state; } }) .handleAction(actions.goToNextPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)) - const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn) + const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); + const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn); - switch (state.viewMode) { - case 'month': { - const firstDayOfNextMonth = firstDayOfMonth.add({ months: 1 }) + switch (state.viewMode.unit) { + case 'months': { + const firstDayOfNextMonth = firstDayOfMonth.add({ months: state.viewMode.value }); return { ...state, currPeriod: firstDayOfNextMonth, - } + }; } - case 'week': { - const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: 1 }) + case 'weeks': { + const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: state.viewMode.value }); return { ...state, currPeriod: firstDayOfNextWeek, - } + }; } - default: { - const nextCustomStart = state.currPeriod.add({ days: state.viewMode }) + case 'days': { + const nextCustomStart = state.currPeriod.add({ days: state.viewMode.value }); return { ...state, currPeriod: nextCustomStart, - } + }; } + default: + return state; } - }) -} + }); +}; export const useCalendarReducer = ( initialState: TState, diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts index dd3679b..b5a48fb 100644 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -1,14 +1,17 @@ -import type { Temporal } from '@js-temporal/polyfill'; +import type { Temporal } from '@js-temporal/polyfill' export interface Event { - id: string; - startDate: Temporal.PlainDateTime; - endDate: Temporal.PlainDateTime; - title: string; + id: string + startDate: Temporal.PlainDateTime + endDate: Temporal.PlainDateTime + title: string } export interface UseCalendarState { - currPeriod: Temporal.PlainDate; - viewMode: 'month' | 'week' | number; - currentTime: Temporal.PlainDateTime; + currPeriod: Temporal.PlainDate + viewMode: { + value: number + unit: 'months' | 'weeks' | 'days' + } + currentTime: Temporal.PlainDateTime } From c2419defb9c6aa6c5794d77a0fd0cecbc1eef00d Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 9 Jun 2024 18:08:58 +0200 Subject: [PATCH 43/95] refactor: change viewMode type --- packages/react-time/src/useCalendar/useCalendar.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 9aaef02..478cfc0 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -234,8 +234,6 @@ export const useCalendar = ({ const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps : 100 - 2 * sidePadding const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - console.log('state.viewMode.unit', state.viewMode.unit) - if (state.viewMode.unit === 'weeks' || state.viewMode.unit === 'days') { return { style: { From 215f7227b6ae8434500cbdb3d93a7ea0ffb0f593 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 9 Jun 2024 18:44:10 +0200 Subject: [PATCH 44/95] fix: missing days in view --- .../react-time/src/useCalendar/useCalendar.ts | 345 ++++++++++++------ 1 file changed, 230 insertions(+), 115 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 478cfc0..06baefa 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,10 +1,11 @@ +import type { CSSProperties } from 'react' import { useCallback, useEffect, useMemo, useTransition } from 'react' import { Temporal } from '@js-temporal/polyfill' + +import type { UseCalendarAction } from './calendarActions' +import type { Event, UseCalendarState } from './useCalendarState' import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' -import type { UseCalendarAction} from './calendarActions'; -import type { Event, UseCalendarState } from './useCalendarState' -import type { CSSProperties } from 'react' export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { const date = Temporal.PlainDate.from(currWeek) @@ -14,7 +15,10 @@ export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { export const getFirstDayOfMonth = (currMonth: string) => Temporal.PlainDate.from(`${currMonth}-01`) -interface UseCalendarProps { +interface UseCalendarProps< + TEvent extends Event, + TState extends UseCalendarState = UseCalendarState, +> { /** * The day of the week the calendar should start on (1 for Monday, 7 for Sunday). * @default 1 @@ -39,7 +43,10 @@ interface UseCalendarProps(state: TState, action: TAction) => TState + reducer?: ( + state: TState, + action: TAction, + ) => TState } const getChunks = function* (arr: T[], n: number) { @@ -54,12 +61,33 @@ const splitMultiDayEvents = (event: TEvent): TEvent[] => { const events: TEvent[] = [] let currentDay = startDate - while (Temporal.PlainDate.compare(currentDay.toPlainDate(), endDate.toPlainDate()) < 0) { - const startOfCurrentDay = currentDay.with({ hour: 0, minute: 0, second: 0, millisecond: 0 }) - const endOfCurrentDay = currentDay.with({ hour: 23, minute: 59, second: 59, millisecond: 999 }) + while ( + Temporal.PlainDate.compare( + currentDay.toPlainDate(), + endDate.toPlainDate(), + ) < 0 + ) { + const startOfCurrentDay = currentDay.with({ + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }) + const endOfCurrentDay = currentDay.with({ + hour: 23, + minute: 59, + second: 59, + millisecond: 999, + }) - const eventStart = Temporal.PlainDateTime.compare(currentDay, startDate) === 0 ? startDate : startOfCurrentDay - const eventEnd = Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 ? endDate : endOfCurrentDay + const eventStart = + Temporal.PlainDateTime.compare(currentDay, startDate) === 0 + ? startDate + : startOfCurrentDay + const eventEnd = + Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 + ? endDate + : endOfCurrentDay events.push({ ...event, startDate: eventStart, endDate: eventEnd }) @@ -69,7 +97,10 @@ const splitMultiDayEvents = (event: TEvent): TEvent[] => { return events } -const generateDateRange = (start: Temporal.PlainDate, end: Temporal.PlainDate) => { +const generateDateRange = ( + start: Temporal.PlainDate, + end: Temporal.PlainDate, +) => { const dates: Temporal.PlainDate[] = [] let current = start while (Temporal.PlainDate.compare(current, end) <= 0) { @@ -114,28 +145,62 @@ export const useCalendar = ({ }: UseCalendarProps) => { const today = Temporal.Now.plainDateISO() const [isPending, startTransition] = useTransition() - const [state, dispatch] = useCalendarReducer({ - currPeriod: today, - viewMode: initialViewMode, - currentTime: Temporal.Now.plainDateTimeISO(), - }, reducer) - - const firstDayOfMonth = useMemo(() => getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)), [state.currPeriod]) - - const firstDayOfWeek = useMemo(() => getFirstDayOfWeek(state.currPeriod.toString(), weekStartsOn), [state.currPeriod, weekStartsOn]) + const [state, dispatch] = useCalendarReducer( + { + currPeriod: today, + viewMode: initialViewMode, + currentTime: Temporal.Now.plainDateTimeISO(), + }, + reducer, + ) + + const firstDayOfMonth = useMemo( + () => + getFirstDayOfMonth( + state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + ), + [state.currPeriod], + ) + + const firstDayOfWeek = useMemo( + () => getFirstDayOfWeek(state.currPeriod.toString(), weekStartsOn), + [state.currPeriod, weekStartsOn], + ) const days = useMemo(() => { - const start = state.viewMode.unit === 'months' ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 }) : firstDayOfWeek; - const end = state.viewMode.unit === 'months' ? firstDayOfMonth.add({ months: state.viewMode.value }).subtract({ days: 1 }) : firstDayOfWeek.add({ days: 6 }); - return Array.from(getChunks(generateDateRange(start, end), 7)).flat(); - }, [state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn]); + const start = + state.viewMode.unit === 'months' + ? firstDayOfMonth.subtract({ + days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7, + }) + : firstDayOfWeek + + let end + if (state.viewMode.unit === 'months') { + const lastDayOfMonth = firstDayOfMonth + .add({ months: state.viewMode.value }) + .subtract({ days: 1 }) + const lastDayOfMonthWeekDay = + (lastDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 + end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }) + } else { + end = firstDayOfWeek.add({ days: 6 }) + } + + return Array.from(getChunks(generateDateRange(start, end), 7)).flat() + }, [state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn]) const eventMap = useMemo(() => { const map = new Map() events?.forEach((event) => { const eventStartDate = Temporal.PlainDateTime.from(event.startDate) const eventEndDate = Temporal.PlainDateTime.from(event.endDate) - if (Temporal.PlainDate.compare(eventStartDate.toPlainDate(), eventEndDate.toPlainDate()) !== 0) { + if ( + Temporal.PlainDate.compare( + eventStartDate.toPlainDate(), + eventEndDate.toPlainDate(), + ) !== 0 + ) { const splitEvents = splitMultiDayEvents(event) splitEvents.forEach((splitEvent) => { const splitKey = splitEvent.startDate.toString().split('T')[0] @@ -155,103 +220,147 @@ export const useCalendar = ({ return map }, [events]) - const daysWithEvents = useMemo(() => days.map((day) => { - const dayKey = day.toString() - const dailyEvents = eventMap.get(dayKey) ?? [] - const isInCurrentPeriod = day.month === state.currPeriod.month + const daysWithEvents = useMemo( + () => + days.map((day) => { + const dayKey = day.toString() + const dailyEvents = eventMap.get(dayKey) ?? [] + const isInCurrentPeriod = day.month === state.currPeriod.month return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, isInCurrentPeriod } }), [days, eventMap, state.currPeriod]) const weeks = useMemo(() => state.viewMode.unit === 'months' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents], [state.viewMode, daysWithEvents]) - const goToPreviousPeriod = useCallback(() => startTransition(() => dispatch(actions.goToPreviousPeriod({ weekStartsOn }))), [dispatch, weekStartsOn]) - - const goToNextPeriod = useCallback(() => startTransition(() => dispatch(actions.goToNextPeriod({ weekStartsOn }))), [dispatch, weekStartsOn]) - - const goToCurrentPeriod = useCallback(() => startTransition(() => dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO()))), [dispatch]) - - const goToSpecificPeriod = useCallback((date: Temporal.PlainDate) => startTransition(() => dispatch(actions.setCurrentPeriod(date))), [dispatch]) - - const changeViewMode = useCallback((newViewMode: UseCalendarState['viewMode']) => { - startTransition(() => { - dispatch(actions.setViewMode(newViewMode)); - onChangeViewMode?.(newViewMode); - }); - }, [dispatch, onChangeViewMode]); - - const getEventProps = useCallback((id: Event['id']): { style: CSSProperties } | null => { - const event = [...eventMap.values()].flat().find((currEvent) => currEvent.id === id) - if (!event) return null + const goToPreviousPeriod = useCallback( + () => + startTransition(() => + dispatch(actions.goToPreviousPeriod({ weekStartsOn })), + ), + [dispatch, weekStartsOn], + ) + + const goToNextPeriod = useCallback( + () => + startTransition(() => dispatch(actions.goToNextPeriod({ weekStartsOn }))), + [dispatch, weekStartsOn], + ) + + const goToCurrentPeriod = useCallback( + () => + startTransition(() => + dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())), + ), + [dispatch], + ) + + const goToSpecificPeriod = useCallback( + (date: Temporal.PlainDate) => + startTransition(() => dispatch(actions.setCurrentPeriod(date))), + [dispatch], + ) + + const changeViewMode = useCallback( + (newViewMode: { value: number; unit: 'months' | 'weeks' | 'days' }) => { + startTransition(() => { + dispatch(actions.setViewMode(newViewMode)) + onChangeViewMode?.(newViewMode) + }) + }, + [dispatch, onChangeViewMode], + ) + + const getEventProps = useCallback( + (id: Event['id']): { style: CSSProperties } | null => { + const event = [...eventMap.values()] + .flat() + .find((currEvent) => currEvent.id === id) + if (!event) return null - const eventStartDate = Temporal.PlainDateTime.from(event.startDate) - const eventEndDate = Temporal.PlainDateTime.from(event.endDate) - const isSplitEvent = Temporal.PlainDate.compare(eventStartDate.toPlainDate(), eventEndDate.toPlainDate()) !== 0 - - let percentageOfDay - let eventHeightInMinutes - - if (isSplitEvent) { - const isStartPart = eventStartDate.hour !== 0 || eventStartDate.minute !== 0 - if (isStartPart) { - const eventTimeInMinutes = eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - eventHeightInMinutes = 24 * 60 - eventTimeInMinutes + const eventStartDate = Temporal.PlainDateTime.from(event.startDate) + const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + const isSplitEvent = + Temporal.PlainDate.compare( + eventStartDate.toPlainDate(), + eventEndDate.toPlainDate(), + ) !== 0 + + let percentageOfDay + let eventHeightInMinutes + + if (isSplitEvent) { + const isStartPart = + eventStartDate.hour !== 0 || eventStartDate.minute !== 0 + if (isStartPart) { + const eventTimeInMinutes = + eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + eventHeightInMinutes = 24 * 60 - eventTimeInMinutes + } else { + percentageOfDay = 0 + eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + } } else { - percentageOfDay = 0 - eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + const eventTimeInMinutes = + eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes } - } else { - const eventTimeInMinutes = eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute - eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes - } - - const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) - - const overlappingEvents = [...eventMap.values()].flat().filter((e) => { - const eStartDate = Temporal.PlainDateTime.from(e.startDate) - const eEndDate = Temporal.PlainDateTime.from(e.endDate) - return ( - (e.id !== id && - Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && - Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && - Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && - Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && - Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) - ) - }) - const eventIndex = overlappingEvents.findIndex((e) => e.id === id) - const totalOverlaps = overlappingEvents.length - const sidePadding = 2 - const innerPadding = 2 - const totalInnerPadding = (totalOverlaps - 1) * innerPadding - const availableWidth = 100 - totalInnerPadding - 2 * sidePadding - const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps : 100 - 2 * sidePadding - const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - - if (state.viewMode.unit === 'weeks' || state.viewMode.unit === 'days') { - return { - style: { - position: 'absolute', - top: `min(${percentageOfDay}%, calc(100% - 55px))`, - left: `${eventLeft}%`, - width: `${eventWidth}%`, - margin: 0, - height: `${eventHeight}%`, - }, + const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) + + const overlappingEvents = [...eventMap.values()].flat().filter((e) => { + const eStartDate = Temporal.PlainDateTime.from(e.startDate) + const eEndDate = Temporal.PlainDateTime.from(e.endDate) + return ( + (e.id !== id && + Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) + ) + }) + + const eventIndex = overlappingEvents.findIndex((e) => e.id === id) + const totalOverlaps = overlappingEvents.length + const sidePadding = 2 + const innerPadding = 2 + const totalInnerPadding = (totalOverlaps - 1) * innerPadding + const availableWidth = 100 - totalInnerPadding - 2 * sidePadding + const eventWidth = + totalOverlaps > 0 + ? availableWidth / totalOverlaps + : 100 - 2 * sidePadding + const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) + + if (state.viewMode.unit === 'weeks' || state.viewMode.unit === 'days') { + return { + style: { + position: 'absolute', + top: `min(${percentageOfDay}%, calc(100% - 55px))`, + left: `${eventLeft}%`, + width: `${eventWidth}%`, + margin: 0, + height: `${eventHeight}%`, + }, + } } - } - - return null - }, [eventMap, state.viewMode]) + + return null + }, + [eventMap, state.viewMode], + ) useEffect(() => { - const intervalId = setInterval(() => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), 60000) + const intervalId = setInterval( + () => + dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), + 60000, + ) return () => clearInterval(intervalId) }, [dispatch]) @@ -273,14 +382,20 @@ export const useCalendar = ({ const daysNames = useMemo(() => { const baseDate = Temporal.PlainDate.from('2024-01-01') return Array.from({ length: 7 }).map((_, i) => - baseDate.add({ days: (i + weekStartsOn - 1) % 7 }) - .toLocaleString(locale, { weekday: 'short' }) + baseDate + .add({ days: (i + weekStartsOn - 1) % 7 }) + .toLocaleString(locale, { weekday: 'short' }), ) }, [locale, weekStartsOn]) return { ...state, - firstDayOfPeriod: state.viewMode.unit === 'months' ? firstDayOfMonth : state.viewMode.unit === 'weeks' ? firstDayOfWeek : state.currPeriod, + firstDayOfPeriod: + state.viewMode.unit === 'months' + ? firstDayOfMonth + : state.viewMode.unit === 'weeks' + ? firstDayOfWeek + : state.currPeriod, goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, @@ -290,6 +405,6 @@ export const useCalendar = ({ changeViewMode, getEventProps, currentTimeMarkerProps, - isPending + isPending, } -} \ No newline at end of file +} From 0f05c00683774e4128a58d991b83773d9beac605 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 13:16:42 +0200 Subject: [PATCH 45/95] refactor: api --- .../react-time/src/tests/useCalendar.test.tsx | 89 ++-- .../react-time/src/useCalendar/useCalendar.ts | 435 +++++++++++------- .../src/useCalendar/useCalendarReducer.ts | 26 +- .../src/useCalendar/useCalendarState.ts | 2 +- 4 files changed, 348 insertions(+), 204 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index bfecedc..c7753ce 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -1,11 +1,14 @@ import { Temporal } from '@js-temporal/polyfill' -import { afterEach, describe, expect, test, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useCalendar } from '../useCalendar' -import type { UseCalendarAction} from '../useCalendar/calendarActions'; +import type { UseCalendarAction } from '../useCalendar/calendarActions'; import type { UseCalendarState } from '../useCalendar/useCalendarState'; describe('useCalendar', () => { + beforeEach(() => { + vi.setSystemTime(new Date('2024-06-01T11:00:00')); + }); afterEach(() => { vi.useRealTimers(); }); @@ -30,7 +33,7 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), ) expect(result.current.viewMode).toEqual({ value: 1, unit: 'months' }) - expect(result.current.currPeriod.toString()).toBe( + expect(result.current.currentPeriod.toString()).toBe( Temporal.Now.plainDateISO().toString(), ) }) @@ -47,14 +50,9 @@ describe('useCalendar', () => { const expectedPreviousMonth = Temporal.Now.plainDateISO().subtract({ months: 1, }) - const expectedFirstDayOfPreviousMonth = Temporal.PlainDate.from({ - year: expectedPreviousMonth.year, - month: expectedPreviousMonth.month, - day: 1, - }) - expect(result.current.firstDayOfPeriod).toEqual( - expectedFirstDayOfPreviousMonth, + expect(result.current.currentPeriod).toEqual( + expectedPreviousMonth, ) }) @@ -68,13 +66,8 @@ describe('useCalendar', () => { }) const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) - const expectedFirstDayOfNextMonth = Temporal.PlainDate.from({ - year: expectedNextMonth.year, - month: expectedNextMonth.month, - day: 1, - }) - expect(result.current.firstDayOfPeriod).toEqual(expectedFirstDayOfNextMonth) + expect(result.current.currentPeriod).toEqual(expectedNextMonth) }) test('should change view mode correctly', () => { @@ -98,7 +91,7 @@ describe('useCalendar', () => { result.current.goToSpecificPeriod(Temporal.PlainDate.from('2024-06-01')) }) - expect(result.current.currPeriod.toString()).toBe('2024-06-01') + expect(result.current.currentPeriod.toString()).toBe('2024-06-01') }) test('should return the correct props for an event', () => { @@ -166,8 +159,6 @@ describe('useCalendar', () => { }) test('should return the correct props for the current time marker', () => { - vi.setSystemTime(new Date('2024-06-01T11:00:00')); - const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 1, unit: 'weeks' } }), ) @@ -185,13 +176,13 @@ describe('useCalendar', () => { }) test('should render array of days', () => { - vi.setSystemTime(new Date('2024-06-01T11:00:00')); - const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }), ); - const { weeks } = result.current; + const { days } = result.current; + const weeks = result.current.groupDaysBy(days, 'weeks'); + expect(weeks).toHaveLength(5); expect(weeks[0]).toHaveLength(7); @@ -217,12 +208,12 @@ describe('useCalendar', () => { }); test('should correctly mark days as in current period', () => { - vi.setSystemTime(new Date('2024-06-01T11:00:00')); const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) ); - const { weeks } = result.current; + const { days } = result.current; + const weeks = result.current.groupDaysBy(days, 'weeks'); const daysInCurrentPeriod = weeks.flat().map(day => day.isInCurrentPeriod); expect(daysInCurrentPeriod).toEqual([ @@ -242,7 +233,7 @@ describe('useCalendar', () => { result.current.goToSpecificPeriod(specificDate) }) - expect(result.current.currPeriod).toEqual(specificDate) + expect(result.current.currentPeriod).toEqual(specificDate) }) test('should navigate to the previous period correctly', () => { @@ -258,7 +249,7 @@ describe('useCalendar', () => { months: 1, }) - expect(result.current.currPeriod).toEqual(expectedPreviousMonth) + expect(result.current.currentPeriod).toEqual(expectedPreviousMonth) }) test('should navigate to the next period correctly', () => { @@ -272,7 +263,7 @@ describe('useCalendar', () => { const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 1 }) - expect(result.current.currPeriod).toEqual(expectedNextMonth) + expect(result.current.currentPeriod).toEqual(expectedNextMonth) }) test('should reset to the current period correctly', () => { @@ -285,7 +276,7 @@ describe('useCalendar', () => { result.current.goToCurrentPeriod() }) - expect(result.current.currPeriod).toEqual( + expect(result.current.currentPeriod).toEqual( Temporal.Now.plainDateISO(), ) }) @@ -295,7 +286,7 @@ describe('useCalendar', () => { if (action.type === 'SET_NEXT_PERIOD') { return { ...state, - currPeriod: state.currPeriod.add({ months: 2 }), + currentPeriod: state.currentPeriod.add({ months: 2 }), } } @@ -311,6 +302,44 @@ describe('useCalendar', () => { }) const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 2 }) - expect(result.current.currPeriod).toEqual(expectedNextMonth) + expect(result.current.currentPeriod).toEqual(expectedNextMonth) + }); + + test('should group days by months correctly', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: { value: 2, unit: 'months' }, locale: 'en-US' }) + ); + + const { days, groupDaysBy } = result.current; + const months = groupDaysBy(days, 'months'); + + expect(months).toHaveLength(2); + expect(months[0]?.[0]?.date.toString()).toBe('2024-06-01'); + expect(months[1]?.[0]?.date.toString()).toBe('2024-07-01'); + }); + + test('should group days by weeks correctly', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) + ); + + const { days, groupDaysBy } = result.current; + const weeks = groupDaysBy(days, 'weeks'); + expect(weeks).toHaveLength(5); + expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-27'); + expect(weeks[4]?.[6]?.date.toString()).toBe('2024-06-30'); + }); + + test('should group days by weeks correctly when weekStartsOn is Sunday', () => { + const { result } = renderHook(() => + useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US', weekStartsOn: 7 }) + ); + + const { days, groupDaysBy } = result.current; + const weeks = groupDaysBy(days, 'weeks'); + + expect(weeks).toHaveLength(6); + expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-26'); + expect(weeks[4]?.[6]?.date.toString()).toBe('2024-06-29'); }); }); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 06baefa..082a9f3 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,66 +1,53 @@ -import type { CSSProperties } from 'react' import { useCallback, useEffect, useMemo, useTransition } from 'react' import { Temporal } from '@js-temporal/polyfill' -import type { UseCalendarAction } from './calendarActions' -import type { Event, UseCalendarState } from './useCalendarState' +import { getFirstDayOfMonth, getFirstDayOfWeek } from '@tanstack/time' import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' - -export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { - const date = Temporal.PlainDate.from(currWeek) - return date.subtract({ days: (date.dayOfWeek - weekStartsOn + 7) % 7 }) -} - -export const getFirstDayOfMonth = (currMonth: string) => - Temporal.PlainDate.from(`${currMonth}-01`) +import type { UseCalendarAction } from './calendarActions' +import type { Event, UseCalendarState } from './useCalendarState' +import type { CSSProperties } from 'react' interface UseCalendarProps< TEvent extends Event, TState extends UseCalendarState = UseCalendarState, > { /** - * The day of the week the calendar should start on (1 for Monday, 7 for Sunday). + * The day of the week the calendar should start on (0 for Sunday, 6 for Saturday). * @default 1 */ - weekStartsOn?: number + weekStartsOn?: number; /** * An array of events that the calendar should display. */ - events?: TEvent[] + events?: TEvent[]; /** * The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. */ - viewMode: UseCalendarState['viewMode'] + viewMode: UseCalendarState['viewMode']; /** * The locale to use for formatting dates and times. */ - locale?: Parameters['0'] + locale?: Parameters['0']; /** * Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. */ - onChangeViewMode?: (viewMode: UseCalendarState['viewMode']) => void + onChangeViewMode?: (viewMode: UseCalendarState['viewMode']) => void; /** * Custom reducer function to manage the state of the calendar. */ reducer?: ( state: TState, action: TAction, - ) => TState -} - -const getChunks = function* (arr: T[], n: number) { - for (let i = 0; i < arr.length; i += n) { - yield arr.slice(i, i + n) - } + ) => TState; } const splitMultiDayEvents = (event: TEvent): TEvent[] => { - const startDate = Temporal.PlainDateTime.from(event.startDate) - const endDate = Temporal.PlainDateTime.from(event.endDate) - const events: TEvent[] = [] + const startDate = Temporal.PlainDateTime.from(event.startDate); + const endDate = Temporal.PlainDateTime.from(event.endDate); + const events: TEvent[] = []; - let currentDay = startDate + let currentDay = startDate; while ( Temporal.PlainDate.compare( currentDay.toPlainDate(), @@ -72,42 +59,42 @@ const splitMultiDayEvents = (event: TEvent): TEvent[] => { minute: 0, second: 0, millisecond: 0, - }) + }); const endOfCurrentDay = currentDay.with({ hour: 23, minute: 59, second: 59, millisecond: 999, - }) + }); const eventStart = Temporal.PlainDateTime.compare(currentDay, startDate) === 0 ? startDate - : startOfCurrentDay + : startOfCurrentDay; const eventEnd = Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 ? endDate - : endOfCurrentDay + : endOfCurrentDay; - events.push({ ...event, startDate: eventStart, endDate: eventEnd }) + events.push({ ...event, startDate: eventStart, endDate: eventEnd }); - currentDay = startOfCurrentDay.add({ days: 1 }) + currentDay = startOfCurrentDay.add({ days: 1 }); } - return events + return events; } const generateDateRange = ( start: Temporal.PlainDate, end: Temporal.PlainDate, ) => { - const dates: Temporal.PlainDate[] = [] - let current = start + const dates: Temporal.PlainDate[] = []; + let current = start; while (Temporal.PlainDate.compare(current, end) <= 0) { - dates.push(current) - current = current.add({ days: 1 }) + dates.push(current); + current = current.add({ days: 1 }); } - return dates + return dates; } /** @@ -122,13 +109,12 @@ const generateDateRange = ( * @param {Function} [props.reducer] - Custom reducer function to manage the state of the calendar. * * @returns {Object} calendarState - The state and functions for managing the calendar. - * @returns {Temporal.PlainDate} calendarState.firstDayOfPeriod - The first day of the current period displayed by the calendar. - * @returns {Temporal.PlainDate} calendarState.currPeriod - The current period displayed by the calendar. + * @returns {Temporal.PlainDate} calendarState.currentPeriod - The current period displayed by the calendar. * @returns {Function} calendarState.goToPreviousPeriod - Function to navigate to the previous period. * @returns {Function} calendarState.goToNextPeriod - Function to navigate to the next period. * @returns {Function} calendarState.goToCurrentPeriod - Function to navigate to the current period. * @returns {Function} calendarState.goToSpecificPeriod - Function to navigate to a specific period. - * @returns {Array>} calendarState.weeks - The calendar grid, where each cell contains the date and events for that day. + * @returns {Array>} calendarState.days - The calendar grid, where each cell contains the date and events for that day. * @returns {string[]} calendarState.daysNames - An array of day names based on the locale and week start day. * @returns {'month' | 'week' | number} calendarState.viewMode - The current view mode of the calendar. * @returns {Function} calendarState.changeViewMode - Function to change the view mode of the calendar. @@ -143,93 +129,126 @@ export const useCalendar = ({ onChangeViewMode, reducer, }: UseCalendarProps) => { - const today = Temporal.Now.plainDateISO() - const [isPending, startTransition] = useTransition() + const today = Temporal.Now.plainDateISO(); + const [isPending, startTransition] = useTransition(); const [state, dispatch] = useCalendarReducer( { - currPeriod: today, + currentPeriod: today, viewMode: initialViewMode, currentTime: Temporal.Now.plainDateTimeISO(), }, reducer, - ) + ); const firstDayOfMonth = useMemo( () => getFirstDayOfMonth( - state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + state.currentPeriod.toString({ calendarName: "auto" }).substring(0, 7), ), - [state.currPeriod], - ) + [state.currentPeriod], + ); const firstDayOfWeek = useMemo( - () => getFirstDayOfWeek(state.currPeriod.toString(), weekStartsOn), - [state.currPeriod, weekStartsOn], - ) + () => getFirstDayOfWeek(state.currentPeriod.toString(), weekStartsOn), + [state.currentPeriod, weekStartsOn], + ); - const days = useMemo(() => { + const calendarDays = useMemo(() => { const start = - state.viewMode.unit === 'months' + state.viewMode.unit === "months" ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7, }) - : firstDayOfWeek - - let end - if (state.viewMode.unit === 'months') { - const lastDayOfMonth = firstDayOfMonth - .add({ months: state.viewMode.value }) - .subtract({ days: 1 }) - const lastDayOfMonthWeekDay = - (lastDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 - end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }) - } else { - end = firstDayOfWeek.add({ days: 6 }) + : state.currentPeriod; + + let end; + switch (state.viewMode.unit) { + case "months": { + const lastDayOfMonth = firstDayOfMonth + .add({ months: state.viewMode.value }) + .subtract({ days: 1 }); + const lastDayOfMonthWeekDay = + (lastDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7; + end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }); + break; + } + case "weeks": { + end = firstDayOfWeek.add({ days: 7 * state.viewMode.value - 1 }); + break; + } + case "days": { + end = state.currentPeriod.add({ days: state.viewMode.value - 1 }); + break; + } } - return Array.from(getChunks(generateDateRange(start, end), 7)).flat() - }, [state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn]) + const allDays = generateDateRange(start, end); + const startMonth = state.currentPeriod.month; + const endMonth = state.currentPeriod.add({ + months: state.viewMode.value - 1, + }).month; + + return allDays.filter( + (day) => day.month >= startMonth && day.month <= endMonth, + ); + }, [ + state.viewMode, + firstDayOfMonth, + firstDayOfWeek, + weekStartsOn, + state.currentPeriod, + ]); const eventMap = useMemo(() => { - const map = new Map() + const map = new Map(); events?.forEach((event) => { - const eventStartDate = Temporal.PlainDateTime.from(event.startDate) - const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + const eventStartDate = Temporal.PlainDateTime.from(event.startDate); + const eventEndDate = Temporal.PlainDateTime.from(event.endDate); if ( Temporal.PlainDate.compare( eventStartDate.toPlainDate(), eventEndDate.toPlainDate(), ) !== 0 ) { - const splitEvents = splitMultiDayEvents(event) + const splitEvents = splitMultiDayEvents(event); splitEvents.forEach((splitEvent) => { - const splitKey = splitEvent.startDate.toString().split('T')[0] + const splitKey = splitEvent.startDate.toString().split("T")[0]; if (splitKey) { - if (!map.has(splitKey)) map.set(splitKey, []) - map.get(splitKey)?.push(splitEvent) + if (!map.has(splitKey)) map.set(splitKey, []); + map.get(splitKey)?.push(splitEvent); } - }) + }); } else { - const eventKey = event.startDate.toString().split('T')[0] + const eventKey = event.startDate.toString().split("T")[0]; if (eventKey) { - if (!map.has(eventKey)) map.set(eventKey, []) - map.get(eventKey)?.push(event) + if (!map.has(eventKey)) map.set(eventKey, []); + map.get(eventKey)?.push(event); } } - }) - return map - }, [events]) + }); + return map; + }, [events]); const daysWithEvents = useMemo( () => - days.map((day) => { - const dayKey = day.toString() - const dailyEvents = eventMap.get(dayKey) ?? [] - const isInCurrentPeriod = day.month === state.currPeriod.month - return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, isInCurrentPeriod } - }), [days, eventMap, state.currPeriod]) - - const weeks = useMemo(() => state.viewMode.unit === 'months' ? [...getChunks(daysWithEvents, 7)] : [daysWithEvents], [state.viewMode, daysWithEvents]) + calendarDays.map((day) => { + const dayKey = day.toString(); + const dailyEvents = eventMap.get(dayKey) ?? []; + const currentMonthRange = Array.from( + { length: state.viewMode.value }, + (_, i) => state.currentPeriod.add({ months: i }).month, + ); + const isInCurrentPeriod = currentMonthRange.includes(day.month); + return { + date: day, + events: dailyEvents, + isToday: + Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, + isInCurrentPeriod, + }; + }), + [calendarDays, eventMap, state.viewMode, state.currentPeriod], + ); const goToPreviousPeriod = useCallback( () => @@ -237,13 +256,13 @@ export const useCalendar = ({ dispatch(actions.goToPreviousPeriod({ weekStartsOn })), ), [dispatch, weekStartsOn], - ) + ); const goToNextPeriod = useCallback( () => startTransition(() => dispatch(actions.goToNextPeriod({ weekStartsOn }))), [dispatch, weekStartsOn], - ) + ); const goToCurrentPeriod = useCallback( () => @@ -251,67 +270,70 @@ export const useCalendar = ({ dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())), ), [dispatch], - ) + ); const goToSpecificPeriod = useCallback( (date: Temporal.PlainDate) => startTransition(() => dispatch(actions.setCurrentPeriod(date))), [dispatch], - ) + ); const changeViewMode = useCallback( - (newViewMode: { value: number; unit: 'months' | 'weeks' | 'days' }) => { + (newViewMode: { value: number; unit: "months" | "weeks" | "days" }) => { startTransition(() => { - dispatch(actions.setViewMode(newViewMode)) - onChangeViewMode?.(newViewMode) - }) + dispatch(actions.setViewMode(newViewMode)); + onChangeViewMode?.(newViewMode); + }); }, [dispatch, onChangeViewMode], - ) + ); const getEventProps = useCallback( - (id: Event['id']): { style: CSSProperties } | null => { + (id: Event["id"]): { style: CSSProperties } | null => { const event = [...eventMap.values()] .flat() - .find((currEvent) => currEvent.id === id) - if (!event) return null + .find((currEvent) => currEvent.id === id); + if (!event) return null; - const eventStartDate = Temporal.PlainDateTime.from(event.startDate) - const eventEndDate = Temporal.PlainDateTime.from(event.endDate) + const eventStartDate = Temporal.PlainDateTime.from(event.startDate); + const eventEndDate = Temporal.PlainDateTime.from(event.endDate); const isSplitEvent = Temporal.PlainDate.compare( eventStartDate.toPlainDate(), eventEndDate.toPlainDate(), - ) !== 0 + ) !== 0; - let percentageOfDay - let eventHeightInMinutes + let percentageOfDay; + let eventHeightInMinutes; if (isSplitEvent) { const isStartPart = - eventStartDate.hour !== 0 || eventStartDate.minute !== 0 + eventStartDate.hour !== 0 || eventStartDate.minute !== 0; if (isStartPart) { const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - eventHeightInMinutes = 24 * 60 - eventTimeInMinutes + eventStartDate.hour * 60 + eventStartDate.minute; + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100; + eventHeightInMinutes = 24 * 60 - eventTimeInMinutes; } else { - percentageOfDay = 0 - eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + percentageOfDay = 0; + eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute; } } else { const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute - eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes + eventStartDate.hour * 60 + eventStartDate.minute; + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100; + const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute; + eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes; } - const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) + const eventHeight = Math.min( + (eventHeightInMinutes / (24 * 60)) * 100, + 20, + ); const overlappingEvents = [...eventMap.values()].flat().filter((e) => { - const eStartDate = Temporal.PlainDateTime.from(e.startDate) - const eEndDate = Temporal.PlainDateTime.from(e.endDate) + const eStartDate = Temporal.PlainDateTime.from(e.startDate); + const eEndDate = Temporal.PlainDateTime.from(e.endDate); return ( (e.id !== id && Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && @@ -322,89 +344,182 @@ export const useCalendar = ({ Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) - ) - }) - - const eventIndex = overlappingEvents.findIndex((e) => e.id === id) - const totalOverlaps = overlappingEvents.length - const sidePadding = 2 - const innerPadding = 2 - const totalInnerPadding = (totalOverlaps - 1) * innerPadding - const availableWidth = 100 - totalInnerPadding - 2 * sidePadding + ); + }); + + const eventIndex = overlappingEvents.findIndex((e) => e.id === id); + const totalOverlaps = overlappingEvents.length; + const sidePadding = 2; + const innerPadding = 2; + const totalInnerPadding = (totalOverlaps - 1) * innerPadding; + const availableWidth = 100 - totalInnerPadding - 2 * sidePadding; const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps - : 100 - 2 * sidePadding - const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) + : 100 - 2 * sidePadding; + const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding); - if (state.viewMode.unit === 'weeks' || state.viewMode.unit === 'days') { + if (state.viewMode.unit === "weeks" || state.viewMode.unit === "days") { return { style: { - position: 'absolute', + position: "absolute", top: `min(${percentageOfDay}%, calc(100% - 55px))`, left: `${eventLeft}%`, width: `${eventWidth}%`, margin: 0, height: `${eventHeight}%`, }, - } + }; } - return null + return null; }, [eventMap, state.viewMode], - ) + ); useEffect(() => { const intervalId = setInterval( () => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), 60000, - ) - return () => clearInterval(intervalId) - }, [dispatch]) + ); + return () => clearInterval(intervalId); + }, [dispatch]); const currentTimeMarkerProps = useCallback(() => { - const { hour, minute } = state.currentTime - const currentTimeInMinutes = hour * 60 + minute - const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 + const { hour, minute } = state.currentTime; + const currentTimeInMinutes = hour * 60 + minute; + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; return { style: { - position: 'absolute', + position: "absolute", top: `${percentageOfDay}%`, left: 0, }, - currentTime: state.currentTime.toString().split('T')[1]?.substring(0, 5), - } - }, [state.currentTime]) + currentTime: state.currentTime.toString().split("T")[1]?.substring(0, 5), + }; + }, [state.currentTime]); const daysNames = useMemo(() => { - const baseDate = Temporal.PlainDate.from('2024-01-01') + const baseDate = Temporal.PlainDate.from("2024-01-01"); return Array.from({ length: 7 }).map((_, i) => baseDate .add({ days: (i + weekStartsOn - 1) % 7 }) - .toLocaleString(locale, { weekday: 'short' }), - ) - }, [locale, weekStartsOn]) + .toLocaleString(locale, { weekday: "short" }), + ); + }, [locale, weekStartsOn]); + + const groupDaysBy = useCallback( + ( + days: { + date: Temporal.PlainDate; + events: TEvent[]; + isToday: boolean; + isInCurrentPeriod: boolean; + }[], + unit: "months" | "weeks", + ) => { + const groups: { + date: Temporal.PlainDate; + events: TEvent[]; + isToday: boolean; + isInCurrentPeriod: boolean; + }[][] = []; + + switch (unit) { + case "months": { + let currentMonth: { + date: Temporal.PlainDate; + events: TEvent[]; + isToday: boolean; + isInCurrentPeriod: boolean; + }[] = []; + days.forEach((day) => { + if ( + currentMonth.length > 0 && + day.date.month !== currentMonth[0]?.date.month + ) { + groups.push(currentMonth); + currentMonth = []; + } + currentMonth.push(day); + }); + if (currentMonth.length > 0) { + groups.push(currentMonth); + } + break; + } + + case 'weeks': { + const weeks: { + date: Temporal.PlainDate; + events: TEvent[]; + isToday: boolean; + isInCurrentPeriod: boolean; + }[][] = []; + let currentWeek: { + date: Temporal.PlainDate; + events: TEvent[]; + isToday: boolean; + isInCurrentPeriod: boolean; + }[] = []; + + days.forEach((day) => { + if (currentWeek.length === 0 && day.date.dayOfWeek !== weekStartsOn) { + const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7; + for (let i = 0; i < dayOfWeek; i++) { + currentWeek.push({ + date: day.date.subtract({ days: dayOfWeek - i }), + events: [], + isToday: false, + isInCurrentPeriod: false, + }); + } + } + currentWeek.push(day); + if (currentWeek.length === 7) { + weeks.push(currentWeek); + currentWeek = []; + } + }); + + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + const lastDate = + currentWeek[currentWeek.length - 1]?.date ?? + Temporal.PlainDate.from("2024-01-01"); + currentWeek.push({ + date: lastDate.add({ days: 1 }), + events: [], + isToday: false, + isInCurrentPeriod: false, + }); + } + weeks.push(currentWeek); + } + + return weeks; + } + default: break + } + return groups; + }, + [weekStartsOn], + ); return { ...state, - firstDayOfPeriod: - state.viewMode.unit === 'months' - ? firstDayOfMonth - : state.viewMode.unit === 'weeks' - ? firstDayOfWeek - : state.currPeriod, goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, goToSpecificPeriod, - weeks, + days: daysWithEvents, daysNames, changeViewMode, getEventProps, currentTimeMarkerProps, isPending, - } -} + groupDaysBy, + }; +}; diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts index 793c101..2f7296b 100644 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ b/packages/react-time/src/useCalendar/useCalendarReducer.ts @@ -9,7 +9,7 @@ const createCalendarReducer = (initialState: UseCalendarState) => { return createReducer(initialState) .handleAction(actions.setCurrentPeriod, (state, action) => ({ ...state, - currPeriod: action.payload, + currentPeriod: action.payload, })) .handleAction(actions.setViewMode, (state, action) => ({ ...state, @@ -20,29 +20,29 @@ const createCalendarReducer = (initialState: UseCalendarState) => { currentTime: action.payload, })) .handleAction(actions.goToPreviousPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); - const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn); + const firstDayOfMonth = getFirstDayOfMonth(state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); + const firstDayOfWeek = getFirstDayOfWeek(state.currentPeriod.toString(), action.payload.weekStartsOn); switch (state.viewMode.unit) { case 'months': { const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: state.viewMode.value }); return { ...state, - currPeriod: firstDayOfPrevMonth, + currentPeriod: firstDayOfPrevMonth, }; } case 'weeks': { const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: state.viewMode.value }); return { ...state, - currPeriod: firstDayOfPrevWeek, + currentPeriod: firstDayOfPrevWeek, }; } case 'days': { - const prevCustomStart = state.currPeriod.subtract({ days: state.viewMode.value }); + const prevCustomStart = state.currentPeriod.subtract({ days: state.viewMode.value }); return { ...state, - currPeriod: prevCustomStart, + currentPeriod: prevCustomStart, }; } default: @@ -50,29 +50,29 @@ const createCalendarReducer = (initialState: UseCalendarState) => { } }) .handleAction(actions.goToNextPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth(state.currPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); - const firstDayOfWeek = getFirstDayOfWeek(state.currPeriod.toString(), action.payload.weekStartsOn); + const firstDayOfMonth = getFirstDayOfMonth(state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); + const firstDayOfWeek = getFirstDayOfWeek(state.currentPeriod.toString(), action.payload.weekStartsOn); switch (state.viewMode.unit) { case 'months': { const firstDayOfNextMonth = firstDayOfMonth.add({ months: state.viewMode.value }); return { ...state, - currPeriod: firstDayOfNextMonth, + currentPeriod: firstDayOfNextMonth, }; } case 'weeks': { const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: state.viewMode.value }); return { ...state, - currPeriod: firstDayOfNextWeek, + currentPeriod: firstDayOfNextWeek, }; } case 'days': { - const nextCustomStart = state.currPeriod.add({ days: state.viewMode.value }); + const nextCustomStart = state.currentPeriod.add({ days: state.viewMode.value }); return { ...state, - currPeriod: nextCustomStart, + currentPeriod: nextCustomStart, }; } default: diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts index b5a48fb..e530874 100644 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -8,7 +8,7 @@ export interface Event { } export interface UseCalendarState { - currPeriod: Temporal.PlainDate + currentPeriod: Temporal.PlainDate viewMode: { value: number unit: 'months' | 'weeks' | 'days' From def6499c63f02511f51256c1e2da699028d0ddf5 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 13:16:50 +0200 Subject: [PATCH 46/95] refactor: api --- docs/framework/react/reference/useCalendar.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 5454204..7f3681f 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -40,7 +40,7 @@ export function useCalendar({ - `firstDayOfPeriod: Temporal.PlainDate` - This value represents the first day of the current period displayed by the calendar. -- `currPeriod: string` +- `currentPeriod: string` - This value represents a string that describes the current period displayed by the calendar. - `goToPreviousPeriod: MouseEventHandler` - This function is a click event handler that navigates to the previous period. @@ -69,14 +69,13 @@ export function useCalendar({ - `isPending: boolean` - This value represents whether the calendar is in a pending state. - #### Example Usage ```tsx const CalendarComponent = ({ events }) => { const { firstDayOfPeriod, - currPeriod, + currentPeriod, goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, From 110e8be61fa7319ee8725434af2bf98e7d7f6648 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 14:06:53 +0200 Subject: [PATCH 47/95] fix: types --- packages/react-time/src/useCalendar/useCalendar.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 082a9f3..d8c36cb 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -121,14 +121,14 @@ const generateDateRange = ( * @returns {Function} calendarState.getEventProps - Function to retrieve the style properties for a specific event based on its ID. * @returns {Function} calendarState.currentTimeMarkerProps - Function to retrieve the style properties and current time for the current time marker. */ -export const useCalendar = ({ +export const useCalendar = ({ weekStartsOn = 1, events, viewMode: initialViewMode, locale, onChangeViewMode, reducer, -}: UseCalendarProps) => { +}: UseCalendarProps) => { const today = Temporal.Now.plainDateISO(); const [isPending, startTransition] = useTransition(); const [state, dispatch] = useCalendarReducer( @@ -136,7 +136,7 @@ export const useCalendar = ({ currentPeriod: today, viewMode: initialViewMode, currentTime: Temporal.Now.plainDateTimeISO(), - }, + } as TState, reducer, ); From 3333fcb391311cb20a52ee48a31a9fdfdea701f9 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 14:07:34 +0200 Subject: [PATCH 48/95] fix: types --- packages/react-time/src/useCalendar/useCalendar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index d8c36cb..99c5342 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -24,7 +24,7 @@ interface UseCalendarProps< /** * The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. */ - viewMode: UseCalendarState['viewMode']; + viewMode: TState['viewMode']; /** * The locale to use for formatting dates and times. */ @@ -32,7 +32,7 @@ interface UseCalendarProps< /** * Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. */ - onChangeViewMode?: (viewMode: UseCalendarState['viewMode']) => void; + onChangeViewMode?: (viewMode: TState['viewMode']) => void; /** * Custom reducer function to manage the state of the calendar. */ From 492405bb992af32dcb58639326b4480c8ddc94d4 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 14:25:06 +0200 Subject: [PATCH 49/95] fix: types --- packages/react-time/src/useCalendar/useCalendar.ts | 12 ++++++------ .../react-time/src/useCalendar/useCalendarReducer.ts | 12 ++++++------ .../react-time/src/useCalendar/useCalendarState.ts | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 99c5342..1c43f4b 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -155,7 +155,7 @@ export const useCalendar = { const start = - state.viewMode.unit === "months" + state.viewMode.unit === "month" ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7, }) @@ -163,7 +163,7 @@ export const useCalendar = { + (newViewMode: TState["viewMode"]) => { startTransition(() => { dispatch(actions.setViewMode(newViewMode)); onChangeViewMode?.(newViewMode); @@ -359,7 +359,7 @@ export const useCalendar = { const firstDayOfWeek = getFirstDayOfWeek(state.currentPeriod.toString(), action.payload.weekStartsOn); switch (state.viewMode.unit) { - case 'months': { + case 'month': { const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: state.viewMode.value }); return { ...state, currentPeriod: firstDayOfPrevMonth, }; } - case 'weeks': { + case 'week': { const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: state.viewMode.value }); return { ...state, currentPeriod: firstDayOfPrevWeek, }; } - case 'days': { + case 'day': { const prevCustomStart = state.currentPeriod.subtract({ days: state.viewMode.value }); return { ...state, @@ -54,21 +54,21 @@ const createCalendarReducer = (initialState: UseCalendarState) => { const firstDayOfWeek = getFirstDayOfWeek(state.currentPeriod.toString(), action.payload.weekStartsOn); switch (state.viewMode.unit) { - case 'months': { + case 'month': { const firstDayOfNextMonth = firstDayOfMonth.add({ months: state.viewMode.value }); return { ...state, currentPeriod: firstDayOfNextMonth, }; } - case 'weeks': { + case 'week': { const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: state.viewMode.value }); return { ...state, currentPeriod: firstDayOfNextWeek, }; } - case 'days': { + case 'day': { const nextCustomStart = state.currentPeriod.add({ days: state.viewMode.value }); return { ...state, diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts index e530874..c5a3b10 100644 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ b/packages/react-time/src/useCalendar/useCalendarState.ts @@ -11,7 +11,7 @@ export interface UseCalendarState { currentPeriod: Temporal.PlainDate viewMode: { value: number - unit: 'months' | 'weeks' | 'days' + unit: 'month' | 'week' | 'day' } currentTime: Temporal.PlainDateTime } From 7bee6b43520df3f0c9017e27b9928813644e4b96 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 14:25:36 +0200 Subject: [PATCH 50/95] fix: types --- .../react-time/src/tests/useCalendar.test.tsx | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index c7753ce..b0e07c8 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -30,9 +30,9 @@ describe('useCalendar', () => { test('should initialize with the correct view mode and current period', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' } }), ) - expect(result.current.viewMode).toEqual({ value: 1, unit: 'months' }) + expect(result.current.viewMode).toEqual({ value: 1, unit: 'month' }) expect(result.current.currentPeriod.toString()).toBe( Temporal.Now.plainDateISO().toString(), ) @@ -40,7 +40,7 @@ describe('useCalendar', () => { test('should navigate to the previous period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' } }), ) act(() => { @@ -58,7 +58,7 @@ describe('useCalendar', () => { test('should navigate to the next period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' } }), ) act(() => { @@ -72,19 +72,19 @@ describe('useCalendar', () => { test('should change view mode correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' } }), ) act(() => { - result.current.changeViewMode({ value: 1, unit: 'weeks' }) + result.current.changeViewMode({ value: 1, unit: 'week' }) }) - expect(result.current.viewMode).toEqual({ value: 1, unit: 'weeks' }) + expect(result.current.viewMode).toEqual({ value: 1, unit: 'week' }) }) test('should select a day correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' } }), ) act(() => { @@ -96,7 +96,7 @@ describe('useCalendar', () => { test('should return the correct props for an event', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'weeks' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'week' } }), ) const eventProps = result.current.getEventProps('1') @@ -129,7 +129,7 @@ describe('useCalendar', () => { }, ] const { result } = renderHook(() => - useCalendar({ events: overlappingEvents, viewMode: { value: 1, unit: 'weeks' } }), + useCalendar({ events: overlappingEvents, viewMode: { value: 1, unit: 'week' } }), ) const event1Props = result.current.getEventProps('1') @@ -160,7 +160,7 @@ describe('useCalendar', () => { test('should return the correct props for the current time marker', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'weeks' } }), + useCalendar({ events, viewMode: { value: 1, unit: 'week' } }), ) const currentTimeMarkerProps = result.current.currentTimeMarkerProps() @@ -177,7 +177,7 @@ describe('useCalendar', () => { test('should render array of days', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }), ); const { days } = result.current; @@ -193,14 +193,14 @@ describe('useCalendar', () => { test('should return the correct day names based on weekStartsOn', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US', weekStartsOn: 1 }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US', weekStartsOn: 1 }) ); const { daysNames } = result.current; expect(daysNames).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); const { result: resultSundayStart } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US', weekStartsOn: 7 }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US', weekStartsOn: 7 }) ); const { daysNames: sundayDaysNames } = resultSundayStart.current; @@ -209,7 +209,7 @@ describe('useCalendar', () => { test('should correctly mark days as in current period', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }) ); const { days } = result.current; @@ -226,7 +226,7 @@ describe('useCalendar', () => { }); test('should navigate to a specific period correctly', () => { - const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 1, unit: 'months' } })) + const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 1, unit: 'month' } })) const specificDate = Temporal.PlainDate.from('2024-05-15') act(() => { @@ -238,7 +238,7 @@ describe('useCalendar', () => { test('should navigate to the previous period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }) ) act(() => { @@ -254,7 +254,7 @@ describe('useCalendar', () => { test('should navigate to the next period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }) ) act(() => { @@ -268,7 +268,7 @@ describe('useCalendar', () => { test('should reset to the current period correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }) ) act(() => { @@ -294,7 +294,7 @@ describe('useCalendar', () => { } const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, reducer: customReducer }), + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, reducer: customReducer }), ) act(() => { @@ -307,7 +307,7 @@ describe('useCalendar', () => { test('should group days by months correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 2, unit: 'months' }, locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 2, unit: 'month' }, locale: 'en-US' }) ); const { days, groupDaysBy } = result.current; @@ -320,7 +320,7 @@ describe('useCalendar', () => { test('should group days by weeks correctly', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US' }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }) ); const { days, groupDaysBy } = result.current; @@ -332,7 +332,7 @@ describe('useCalendar', () => { test('should group days by weeks correctly when weekStartsOn is Sunday', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'months' }, locale: 'en-US', weekStartsOn: 7 }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US', weekStartsOn: 7 }) ); const { days, groupDaysBy } = result.current; From 83f0724db1e932d6c6500f1b61a830196894160d Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 14:30:55 +0200 Subject: [PATCH 51/95] docs: useCalendar --- .../react-time/src/useCalendar/useCalendar.ts | 372 +++++++++--------- 1 file changed, 185 insertions(+), 187 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 1c43f4b..fc09c01 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -8,6 +8,13 @@ import type { UseCalendarAction } from './calendarActions' import type { Event, UseCalendarState } from './useCalendarState' import type { CSSProperties } from 'react' +type Day = { + date: Temporal.PlainDate + events: TEvent[] + isToday: boolean + isInCurrentPeriod: boolean +} + interface UseCalendarProps< TEvent extends Event, TState extends UseCalendarState = UseCalendarState, @@ -16,38 +23,38 @@ interface UseCalendarProps< * The day of the week the calendar should start on (0 for Sunday, 6 for Saturday). * @default 1 */ - weekStartsOn?: number; + weekStartsOn?: number /** * An array of events that the calendar should display. */ - events?: TEvent[]; + events?: TEvent[] /** * The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. */ - viewMode: TState['viewMode']; + viewMode: TState['viewMode'] /** * The locale to use for formatting dates and times. */ - locale?: Parameters['0']; + locale?: Parameters['0'] /** * Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. */ - onChangeViewMode?: (viewMode: TState['viewMode']) => void; + onChangeViewMode?: (viewMode: TState['viewMode']) => void /** * Custom reducer function to manage the state of the calendar. */ reducer?: ( state: TState, action: TAction, - ) => TState; + ) => TState } const splitMultiDayEvents = (event: TEvent): TEvent[] => { - const startDate = Temporal.PlainDateTime.from(event.startDate); - const endDate = Temporal.PlainDateTime.from(event.endDate); - const events: TEvent[] = []; + const startDate = Temporal.PlainDateTime.from(event.startDate) + const endDate = Temporal.PlainDateTime.from(event.endDate) + const events: TEvent[] = [] - let currentDay = startDate; + let currentDay = startDate while ( Temporal.PlainDate.compare( currentDay.toPlainDate(), @@ -59,42 +66,42 @@ const splitMultiDayEvents = (event: TEvent): TEvent[] => { minute: 0, second: 0, millisecond: 0, - }); + }) const endOfCurrentDay = currentDay.with({ hour: 23, minute: 59, second: 59, millisecond: 999, - }); + }) const eventStart = Temporal.PlainDateTime.compare(currentDay, startDate) === 0 ? startDate - : startOfCurrentDay; + : startOfCurrentDay const eventEnd = Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 ? endDate - : endOfCurrentDay; + : endOfCurrentDay - events.push({ ...event, startDate: eventStart, endDate: eventEnd }); + events.push({ ...event, startDate: eventStart, endDate: eventEnd }) - currentDay = startOfCurrentDay.add({ days: 1 }); + currentDay = startOfCurrentDay.add({ days: 1 }) } - return events; + return events } const generateDateRange = ( start: Temporal.PlainDate, end: Temporal.PlainDate, ) => { - const dates: Temporal.PlainDate[] = []; - let current = start; + const dates: Temporal.PlainDate[] = [] + let current = start while (Temporal.PlainDate.compare(current, end) <= 0) { - dates.push(current); - current = current.add({ days: 1 }); + dates.push(current) + current = current.add({ days: 1 }) } - return dates; + return dates } /** @@ -121,7 +128,10 @@ const generateDateRange = ( * @returns {Function} calendarState.getEventProps - Function to retrieve the style properties for a specific event based on its ID. * @returns {Function} calendarState.currentTimeMarkerProps - Function to retrieve the style properties and current time for the current time marker. */ -export const useCalendar = ({ +export const useCalendar = < + TEvent extends Event, + TState extends UseCalendarState = UseCalendarState, +>({ weekStartsOn = 1, events, viewMode: initialViewMode, @@ -129,8 +139,8 @@ export const useCalendar = ) => { - const today = Temporal.Now.plainDateISO(); - const [isPending, startTransition] = useTransition(); + const today = Temporal.Now.plainDateISO() + const [isPending, startTransition] = useTransition() const [state, dispatch] = useCalendarReducer( { currentPeriod: today, @@ -138,117 +148,117 @@ export const useCalendar = getFirstDayOfMonth( - state.currentPeriod.toString({ calendarName: "auto" }).substring(0, 7), + state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7), ), [state.currentPeriod], - ); + ) const firstDayOfWeek = useMemo( () => getFirstDayOfWeek(state.currentPeriod.toString(), weekStartsOn), [state.currentPeriod, weekStartsOn], - ); + ) const calendarDays = useMemo(() => { const start = - state.viewMode.unit === "month" + state.viewMode.unit === 'month' ? firstDayOfMonth.subtract({ days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7, }) - : state.currentPeriod; + : state.currentPeriod - let end; + let end switch (state.viewMode.unit) { - case "month": { + case 'month': { const lastDayOfMonth = firstDayOfMonth .add({ months: state.viewMode.value }) - .subtract({ days: 1 }); + .subtract({ days: 1 }) const lastDayOfMonthWeekDay = - (lastDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7; - end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }); - break; + (lastDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 + end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }) + break } - case "week": { - end = firstDayOfWeek.add({ days: 7 * state.viewMode.value - 1 }); - break; + case 'week': { + end = firstDayOfWeek.add({ days: 7 * state.viewMode.value - 1 }) + break } - case "day": { - end = state.currentPeriod.add({ days: state.viewMode.value - 1 }); - break; + case 'day': { + end = state.currentPeriod.add({ days: state.viewMode.value - 1 }) + break } } - const allDays = generateDateRange(start, end); - const startMonth = state.currentPeriod.month; + const allDays = generateDateRange(start, end) + const startMonth = state.currentPeriod.month const endMonth = state.currentPeriod.add({ months: state.viewMode.value - 1, - }).month; + }).month return allDays.filter( (day) => day.month >= startMonth && day.month <= endMonth, - ); + ) }, [ state.viewMode, firstDayOfMonth, firstDayOfWeek, weekStartsOn, state.currentPeriod, - ]); + ]) const eventMap = useMemo(() => { - const map = new Map(); + const map = new Map() events?.forEach((event) => { - const eventStartDate = Temporal.PlainDateTime.from(event.startDate); - const eventEndDate = Temporal.PlainDateTime.from(event.endDate); + const eventStartDate = Temporal.PlainDateTime.from(event.startDate) + const eventEndDate = Temporal.PlainDateTime.from(event.endDate) if ( Temporal.PlainDate.compare( eventStartDate.toPlainDate(), eventEndDate.toPlainDate(), ) !== 0 ) { - const splitEvents = splitMultiDayEvents(event); + const splitEvents = splitMultiDayEvents(event) splitEvents.forEach((splitEvent) => { - const splitKey = splitEvent.startDate.toString().split("T")[0]; + const splitKey = splitEvent.startDate.toString().split('T')[0] if (splitKey) { - if (!map.has(splitKey)) map.set(splitKey, []); - map.get(splitKey)?.push(splitEvent); + if (!map.has(splitKey)) map.set(splitKey, []) + map.get(splitKey)?.push(splitEvent) } - }); + }) } else { - const eventKey = event.startDate.toString().split("T")[0]; + const eventKey = event.startDate.toString().split('T')[0] if (eventKey) { - if (!map.has(eventKey)) map.set(eventKey, []); - map.get(eventKey)?.push(event); + if (!map.has(eventKey)) map.set(eventKey, []) + map.get(eventKey)?.push(event) } } - }); - return map; - }, [events]); + }) + return map + }, [events]) const daysWithEvents = useMemo( () => calendarDays.map((day) => { - const dayKey = day.toString(); - const dailyEvents = eventMap.get(dayKey) ?? []; + const dayKey = day.toString() + const dailyEvents = eventMap.get(dayKey) ?? [] const currentMonthRange = Array.from( { length: state.viewMode.value }, (_, i) => state.currentPeriod.add({ months: i }).month, - ); - const isInCurrentPeriod = currentMonthRange.includes(day.month); + ) + const isInCurrentPeriod = currentMonthRange.includes(day.month) return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, isInCurrentPeriod, - }; + } }), [calendarDays, eventMap, state.viewMode, state.currentPeriod], - ); + ) const goToPreviousPeriod = useCallback( () => @@ -256,13 +266,13 @@ export const useCalendar = startTransition(() => dispatch(actions.goToNextPeriod({ weekStartsOn }))), [dispatch, weekStartsOn], - ); + ) const goToCurrentPeriod = useCallback( () => @@ -270,70 +280,67 @@ export const useCalendar = startTransition(() => dispatch(actions.setCurrentPeriod(date))), [dispatch], - ); + ) const changeViewMode = useCallback( - (newViewMode: TState["viewMode"]) => { + (newViewMode: TState['viewMode']) => { startTransition(() => { - dispatch(actions.setViewMode(newViewMode)); - onChangeViewMode?.(newViewMode); - }); + dispatch(actions.setViewMode(newViewMode)) + onChangeViewMode?.(newViewMode) + }) }, [dispatch, onChangeViewMode], - ); + ) const getEventProps = useCallback( - (id: Event["id"]): { style: CSSProperties } | null => { + (id: Event['id']): { style: CSSProperties } | null => { const event = [...eventMap.values()] .flat() - .find((currEvent) => currEvent.id === id); - if (!event) return null; + .find((currEvent) => currEvent.id === id) + if (!event) return null - const eventStartDate = Temporal.PlainDateTime.from(event.startDate); - const eventEndDate = Temporal.PlainDateTime.from(event.endDate); + const eventStartDate = Temporal.PlainDateTime.from(event.startDate) + const eventEndDate = Temporal.PlainDateTime.from(event.endDate) const isSplitEvent = Temporal.PlainDate.compare( eventStartDate.toPlainDate(), eventEndDate.toPlainDate(), - ) !== 0; + ) !== 0 - let percentageOfDay; - let eventHeightInMinutes; + let percentageOfDay + let eventHeightInMinutes if (isSplitEvent) { const isStartPart = - eventStartDate.hour !== 0 || eventStartDate.minute !== 0; + eventStartDate.hour !== 0 || eventStartDate.minute !== 0 if (isStartPart) { const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute; - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100; - eventHeightInMinutes = 24 * 60 - eventTimeInMinutes; + eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + eventHeightInMinutes = 24 * 60 - eventTimeInMinutes } else { - percentageOfDay = 0; - eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute; + percentageOfDay = 0 + eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute } } else { const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute; - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100; - const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute; - eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes; + eventStartDate.hour * 60 + eventStartDate.minute + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 + const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute + eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes } - const eventHeight = Math.min( - (eventHeightInMinutes / (24 * 60)) * 100, - 20, - ); + const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) const overlappingEvents = [...eventMap.values()].flat().filter((e) => { - const eStartDate = Temporal.PlainDateTime.from(e.startDate); - const eEndDate = Temporal.PlainDateTime.from(e.endDate); + const eStartDate = Temporal.PlainDateTime.from(e.startDate) + const eEndDate = Temporal.PlainDateTime.from(e.endDate) return ( (e.id !== id && Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && @@ -344,169 +351,160 @@ export const useCalendar = = 0 && Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) - ); - }); - - const eventIndex = overlappingEvents.findIndex((e) => e.id === id); - const totalOverlaps = overlappingEvents.length; - const sidePadding = 2; - const innerPadding = 2; - const totalInnerPadding = (totalOverlaps - 1) * innerPadding; - const availableWidth = 100 - totalInnerPadding - 2 * sidePadding; + ) + }) + + const eventIndex = overlappingEvents.findIndex((e) => e.id === id) + const totalOverlaps = overlappingEvents.length + const sidePadding = 2 + const innerPadding = 2 + const totalInnerPadding = (totalOverlaps - 1) * innerPadding + const availableWidth = 100 - totalInnerPadding - 2 * sidePadding const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps - : 100 - 2 * sidePadding; - const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding); + : 100 - 2 * sidePadding + const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - if (state.viewMode.unit === "week" || state.viewMode.unit === "day") { + if (state.viewMode.unit === 'week' || state.viewMode.unit === 'day') { return { style: { - position: "absolute", + position: 'absolute', top: `min(${percentageOfDay}%, calc(100% - 55px))`, left: `${eventLeft}%`, width: `${eventWidth}%`, margin: 0, height: `${eventHeight}%`, }, - }; + } } - return null; + return null }, [eventMap, state.viewMode], - ); + ) useEffect(() => { const intervalId = setInterval( () => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), 60000, - ); - return () => clearInterval(intervalId); - }, [dispatch]); + ) + return () => clearInterval(intervalId) + }, [dispatch]) const currentTimeMarkerProps = useCallback(() => { - const { hour, minute } = state.currentTime; - const currentTimeInMinutes = hour * 60 + minute; - const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; + const { hour, minute } = state.currentTime + const currentTimeInMinutes = hour * 60 + minute + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 return { style: { - position: "absolute", + position: 'absolute', top: `${percentageOfDay}%`, left: 0, }, - currentTime: state.currentTime.toString().split("T")[1]?.substring(0, 5), - }; - }, [state.currentTime]); + currentTime: state.currentTime.toString().split('T')[1]?.substring(0, 5), + } + }, [state.currentTime]) const daysNames = useMemo(() => { - const baseDate = Temporal.PlainDate.from("2024-01-01"); + const baseDate = Temporal.PlainDate.from('2024-01-01') return Array.from({ length: 7 }).map((_, i) => baseDate .add({ days: (i + weekStartsOn - 1) % 7 }) - .toLocaleString(locale, { weekday: "short" }), - ); - }, [locale, weekStartsOn]); + .toLocaleString(locale, { weekday: 'short' }), + ) + }, [locale, weekStartsOn]) const groupDaysBy = useCallback( - ( - days: { - date: Temporal.PlainDate; - events: TEvent[]; - isToday: boolean; - isInCurrentPeriod: boolean; - }[], - unit: "months" | "weeks", - ) => { + (days: Day[], unit: 'months' | 'weeks') => { const groups: { - date: Temporal.PlainDate; - events: TEvent[]; - isToday: boolean; - isInCurrentPeriod: boolean; - }[][] = []; + date: Temporal.PlainDate + events: TEvent[] + isToday: boolean + isInCurrentPeriod: boolean + }[][] = [] switch (unit) { - case "months": { - let currentMonth: { - date: Temporal.PlainDate; - events: TEvent[]; - isToday: boolean; - isInCurrentPeriod: boolean; - }[] = []; + case 'months': { + let currentMonth: Day[] = [] days.forEach((day) => { if ( currentMonth.length > 0 && day.date.month !== currentMonth[0]?.date.month ) { - groups.push(currentMonth); - currentMonth = []; + groups.push(currentMonth) + currentMonth = [] } - currentMonth.push(day); - }); + currentMonth.push(day) + }) if (currentMonth.length > 0) { - groups.push(currentMonth); + groups.push(currentMonth) } - break; + break } case 'weeks': { const weeks: { - date: Temporal.PlainDate; - events: TEvent[]; - isToday: boolean; - isInCurrentPeriod: boolean; - }[][] = []; + date: Temporal.PlainDate + events: TEvent[] + isToday: boolean + isInCurrentPeriod: boolean + }[][] = [] let currentWeek: { - date: Temporal.PlainDate; - events: TEvent[]; - isToday: boolean; - isInCurrentPeriod: boolean; - }[] = []; - + date: Temporal.PlainDate + events: TEvent[] + isToday: boolean + isInCurrentPeriod: boolean + }[] = [] + days.forEach((day) => { - if (currentWeek.length === 0 && day.date.dayOfWeek !== weekStartsOn) { - const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7; + if ( + currentWeek.length === 0 && + day.date.dayOfWeek !== weekStartsOn + ) { + const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7 for (let i = 0; i < dayOfWeek; i++) { currentWeek.push({ date: day.date.subtract({ days: dayOfWeek - i }), events: [], isToday: false, isInCurrentPeriod: false, - }); + }) } } - currentWeek.push(day); + currentWeek.push(day) if (currentWeek.length === 7) { - weeks.push(currentWeek); - currentWeek = []; + weeks.push(currentWeek) + currentWeek = [] } - }); - + }) + if (currentWeek.length > 0) { while (currentWeek.length < 7) { const lastDate = currentWeek[currentWeek.length - 1]?.date ?? - Temporal.PlainDate.from("2024-01-01"); + Temporal.PlainDate.from('2024-01-01') currentWeek.push({ date: lastDate.add({ days: 1 }), events: [], isToday: false, isInCurrentPeriod: false, - }); + }) } - weeks.push(currentWeek); + weeks.push(currentWeek) } - - return weeks; + + return weeks } - default: break + default: + break } - return groups; + return groups }, [weekStartsOn], - ); + ) return { ...state, @@ -521,5 +519,5 @@ export const useCalendar = Date: Tue, 11 Jun 2024 14:31:00 +0200 Subject: [PATCH 52/95] docs: useCalendar --- docs/framework/react/reference/useCalendar.md | 132 ++++++++++++++---- 1 file changed, 104 insertions(+), 28 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 7f3681f..a55d02e 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -50,8 +50,8 @@ export function useCalendar({ - This function is a click event handler that navigates to the current period. - `goToSpecificPeriod: (date: Temporal.PlainDate) => void` - This function is a callback function that is called when a date is selected in the calendar. It receives the selected date as an argument. -- `weeks: Array>` - - This value represents the calendar grid, where each cell contains the date and events for that day. +- `days: Day[]` + - This value represents an array of days in the current period displayed by the calendar. - `daysNames: string[]` - This value represents an array of strings that contain the names of the days of the week. - `viewMode: 'month' | 'week' | number` @@ -68,6 +68,8 @@ export function useCalendar({ - This function is used to retrieve the style properties and current time for the current time marker. - `isPending: boolean` - This value represents whether the calendar is in a pending state. +- `groupDaysBy: (days: Day[], unit: 'day' | 'week' | 'month') => Day[][]` + - This function is used to group days into units of days, weeks, or months. #### Example Usage @@ -81,14 +83,15 @@ const CalendarComponent = ({ events }) => { goToCurrentPeriod, goToSpecificPeriod, changeViewMode, - weeks, + days, daysNames, viewMode, getEventProps, currentTimeMarkerProps, + groupDaysBy, } = useCalendar({ - events, - viewMode: 'month', + weekStartsOn: 1, + viewMode: { value: 1, unit: 'month' }, locale: 'en-US', onChangeViewMode: (newViewMode) => console.log('View mode changed:', newViewMode), }); @@ -101,14 +104,58 @@ const CalendarComponent = ({ events }) => {
- - - - + + + +
- {viewMode === 'month' && ( - + {viewMode.unit === 'month' && ( + groupDaysBy(days, 'months').map((month, monthIndex) => ( + + + + + + {daysNames.map((dayName, index) => ( + + ))} + + {groupDaysBy(month, 'weeks').map((week, weekIndex) => ( + + {week.map((day) => ( + + ))} + + ))} + + )) + )} + + {viewMode.unit === 'week' && ( + {daysNames.map((dayName, index) => ( ))} - + {groupDaysBy(days, 'weeks').map((week, weekIndex) => ( + + {week.map((day) => ( + + ))} + + ))} + )} - - {weeks.map((week, weekIndex) => ( - - {week.map((day) => ( - + + {daysNames.map((dayName, index) => ( + + ))} + + + {days.map((day) => ( + ))} -
- ))} - + + )}
+ {month[0]?.date.toLocaleString('default', { month: 'long' })}{' '} + {month[0]?.date.year} +
+ {dayName} +
+
{day.date.day}
+
+ {day.events.map((event) => ( +
+ {event.title} +
+ ))} +
+
@@ -116,26 +163,56 @@ const CalendarComponent = ({ events }) => {
+
{day.date.day}
+
+ {day.events.map((event) => ( +
+ {event.title} +
+ ))} +
+
-
- {day.date.day} -
+ + {viewMode.unit === 'day' && ( +
+ {dayName} +
+
{day.date.day}
{day.events.map((event) => (
{event.title}
@@ -143,10 +220,9 @@ const CalendarComponent = ({ events }) => {
); From e598c204b5bb929c759752e6f6668454141786bb Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 15:05:39 +0200 Subject: [PATCH 53/95] fix: types --- docs/framework/react/reference/useCalendar.md | 4 ++-- packages/react-time/src/useCalendar/useCalendar.ts | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index a55d02e..b54df89 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -28,9 +28,9 @@ export function useCalendar({ - This parameter is a string that specifies the initial view mode of the calendar. It can be either 'month', 'week', or a number representing the number of days in a custom view mode. - `locale?: string` - This parameter is an optional string that specifies the locale to use for formatting dates and times. It defaults to the system locale. -- `onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void` +- `onChangeViewMode?: ({ value: number; unit: "month" | "week" | "day"; }) => void` - This parameter is an optional callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. -- `onChangeViewMode?: (viewMode: 'month' | 'week' | number) => void` +- `onChangeViewMode?: (viewMode: value: number; unit: "month" | "week" | "day";) => void` - This parameter is an optional callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. - `reducer?: (state: CalendarState, action: CalendarAction) => CalendarState` - This parameter is an optional custom reducer function that can be used to manage the state of the calendar. diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index fc09c01..6968534 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -419,12 +419,7 @@ export const useCalendar = < const groupDaysBy = useCallback( (days: Day[], unit: 'months' | 'weeks') => { - const groups: { - date: Temporal.PlainDate - events: TEvent[] - isToday: boolean - isInCurrentPeriod: boolean - }[][] = [] + const groups: Day[][] = [] switch (unit) { case 'months': { From a9665a72509801a77d1064ba4a7c083c52670829 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 19:17:02 +0200 Subject: [PATCH 54/95] refactor: add the fillMissingDays argument --- docs/framework/react/reference/useCalendar.md | 4 +- .../react-time/src/tests/useCalendar.test.tsx | 4 +- .../react-time/src/useCalendar/useCalendar.ts | 97 +++++++++---------- 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index b54df89..4984687 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -68,8 +68,8 @@ export function useCalendar({ - This function is used to retrieve the style properties and current time for the current time marker. - `isPending: boolean` - This value represents whether the calendar is in a pending state. -- `groupDaysBy: (days: Day[], unit: 'day' | 'week' | 'month') => Day[][]` - - This function is used to group days into units of days, weeks, or months. +- `groupDaysBy: (days: Day[], unit: 'day' | 'week' | 'month', fillMissingDays?: boolean) => Day[][]` + - This function is used to group the days in the current period by a specified unit. #### Example Usage diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index b0e07c8..7e02f13 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -188,7 +188,7 @@ describe('useCalendar', () => { expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-27'); expect(weeks[weeks.length - 1]?.[0]?.date.toString()).toBe('2024-06-24'); - expect(weeks.find((week) => week.some((day) => day.isToday))?.find((day) => day.isToday)?.date.toString()).toBe('2024-06-01'); + expect(weeks.find((week) => week.some((day) => day?.isToday))?.find((day) => day?.isToday)?.date.toString()).toBe('2024-06-01'); }); test('should return the correct day names based on weekStartsOn', () => { @@ -214,7 +214,7 @@ describe('useCalendar', () => { const { days } = result.current; const weeks = result.current.groupDaysBy(days, 'weeks'); - const daysInCurrentPeriod = weeks.flat().map(day => day.isInCurrentPeriod); + const daysInCurrentPeriod = weeks.flat().map(day => day?.isInCurrentPeriod); expect(daysInCurrentPeriod).toEqual([ false, false, false, false, false, true, true, diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 6968534..9c812c5 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -418,88 +418,81 @@ export const useCalendar = < }, [locale, weekStartsOn]) const groupDaysBy = useCallback( - (days: Day[], unit: 'months' | 'weeks') => { - const groups: Day[][] = [] - + ( + days: (Day | null)[], + unit: "months" | "weeks", + fillMissingDays = true + ) => { + const groups: (Day | null)[][] = []; + switch (unit) { - case 'months': { - let currentMonth: Day[] = [] + case "months": { + let currentMonth: (Day | null)[] = []; days.forEach((day) => { if ( currentMonth.length > 0 && - day.date.month !== currentMonth[0]?.date.month + day?.date.month !== currentMonth[0]?.date.month ) { - groups.push(currentMonth) - currentMonth = [] + groups.push(currentMonth); + currentMonth = []; } - currentMonth.push(day) - }) + currentMonth.push(day); + }); if (currentMonth.length > 0) { - groups.push(currentMonth) + groups.push(currentMonth); } - break + break; } - + case 'weeks': { - const weeks: { - date: Temporal.PlainDate - events: TEvent[] - isToday: boolean - isInCurrentPeriod: boolean - }[][] = [] - let currentWeek: { - date: Temporal.PlainDate - events: TEvent[] - isToday: boolean - isInCurrentPeriod: boolean - }[] = [] - + const weeks: (Day | null)[][] = []; + let currentWeek: (Day | null)[] = []; + days.forEach((day) => { - if ( - currentWeek.length === 0 && - day.date.dayOfWeek !== weekStartsOn - ) { - const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7 + if (currentWeek.length === 0 && day?.date.dayOfWeek !== weekStartsOn) { + if (day) { + const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7; for (let i = 0; i < dayOfWeek; i++) { - currentWeek.push({ - date: day.date.subtract({ days: dayOfWeek - i }), - events: [], - isToday: false, - isInCurrentPeriod: false, - }) + currentWeek.push(fillMissingDays ? { + date: day.date.subtract({ days: dayOfWeek - i }), + events: [], + isToday: false, + isInCurrentPeriod: false, + } : null); + } } } - currentWeek.push(day) + currentWeek.push(day); if (currentWeek.length === 7) { - weeks.push(currentWeek) - currentWeek = [] + weeks.push(currentWeek); + currentWeek = []; } - }) - + }); + if (currentWeek.length > 0) { while (currentWeek.length < 7) { const lastDate = currentWeek[currentWeek.length - 1]?.date ?? - Temporal.PlainDate.from('2024-01-01') - currentWeek.push({ + Temporal.PlainDate.from("2024-01-01"); + currentWeek.push(fillMissingDays ? { date: lastDate.add({ days: 1 }), events: [], isToday: false, isInCurrentPeriod: false, - }) + } : null); } - weeks.push(currentWeek) + weeks.push(currentWeek); } - - return weeks + + return weeks; } - default: - break + default: break } - return groups + return groups; }, [weekStartsOn], - ) + ); + return { ...state, From d9e9b883faa8ef3292b98a8f2d59f23049d6a4f5 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 22:08:50 +0200 Subject: [PATCH 55/95] refactor: add the fillMissingDays argument --- .../react-time/src/tests/useCalendar.test.tsx | 10 +- .../react-time/src/useCalendar/useCalendar.ts | 117 +++++++++++------- 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 7e02f13..68a1be7 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -181,7 +181,7 @@ describe('useCalendar', () => { ); const { days } = result.current; - const weeks = result.current.groupDaysBy(days, 'weeks'); + const weeks = result.current.groupDaysBy({ days, unit: 'week' }); expect(weeks).toHaveLength(5); expect(weeks[0]).toHaveLength(7); @@ -213,7 +213,7 @@ describe('useCalendar', () => { ); const { days } = result.current; - const weeks = result.current.groupDaysBy(days, 'weeks'); + const weeks = result.current.groupDaysBy({ days, unit: 'week' }); const daysInCurrentPeriod = weeks.flat().map(day => day?.isInCurrentPeriod); expect(daysInCurrentPeriod).toEqual([ @@ -311,7 +311,7 @@ describe('useCalendar', () => { ); const { days, groupDaysBy } = result.current; - const months = groupDaysBy(days, 'months'); + const months = groupDaysBy({ days, unit: 'month' }); expect(months).toHaveLength(2); expect(months[0]?.[0]?.date.toString()).toBe('2024-06-01'); @@ -324,7 +324,7 @@ describe('useCalendar', () => { ); const { days, groupDaysBy } = result.current; - const weeks = groupDaysBy(days, 'weeks'); + const weeks = groupDaysBy({ days, unit: 'week' }); expect(weeks).toHaveLength(5); expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-27'); expect(weeks[4]?.[6]?.date.toString()).toBe('2024-06-30'); @@ -336,7 +336,7 @@ describe('useCalendar', () => { ); const { days, groupDaysBy } = result.current; - const weeks = groupDaysBy(days, 'weeks'); + const weeks = groupDaysBy({ days, unit: 'week' }); expect(weeks).toHaveLength(6); expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-26'); diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 9c812c5..72c12c1 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -418,81 +418,102 @@ export const useCalendar = < }, [locale, weekStartsOn]) const groupDaysBy = useCallback( - ( - days: (Day | null)[], - unit: "months" | "weeks", - fillMissingDays = true - ) => { - const groups: (Day | null)[][] = []; - + ({ + days, + unit, + fillMissingDays = true, + }: + | { + days: (Day | null)[] + unit: 'month' + fillMissingDays?: never + } + | { + days: (Day | null)[] + unit: 'week' + fillMissingDays?: boolean + }) => { + const groups: (Day | null)[][] = [] + switch (unit) { - case "months": { - let currentMonth: (Day | null)[] = []; + case 'month': { + let currentMonth: (Day | null)[] = [] days.forEach((day) => { if ( currentMonth.length > 0 && day?.date.month !== currentMonth[0]?.date.month ) { - groups.push(currentMonth); - currentMonth = []; + groups.push(currentMonth) + currentMonth = [] } - currentMonth.push(day); - }); + currentMonth.push(day) + }) if (currentMonth.length > 0) { - groups.push(currentMonth); + groups.push(currentMonth) } - break; + break } - - case 'weeks': { - const weeks: (Day | null)[][] = []; - let currentWeek: (Day | null)[] = []; - + + case 'week': { + const weeks: (Day | null)[][] = [] + let currentWeek: (Day | null)[] = [] + days.forEach((day) => { - if (currentWeek.length === 0 && day?.date.dayOfWeek !== weekStartsOn) { + if ( + currentWeek.length === 0 && + day?.date.dayOfWeek !== weekStartsOn + ) { if (day) { - const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7; - for (let i = 0; i < dayOfWeek; i++) { - currentWeek.push(fillMissingDays ? { - date: day.date.subtract({ days: dayOfWeek - i }), - events: [], - isToday: false, - isInCurrentPeriod: false, - } : null); + const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7 + for (let i = 0; i < dayOfWeek; i++) { + currentWeek.push( + fillMissingDays + ? { + date: day.date.subtract({ days: dayOfWeek - i }), + events: [], + isToday: false, + isInCurrentPeriod: false, + } + : null, + ) } } } - currentWeek.push(day); + currentWeek.push(day) if (currentWeek.length === 7) { - weeks.push(currentWeek); - currentWeek = []; + weeks.push(currentWeek) + currentWeek = [] } - }); - + }) + if (currentWeek.length > 0) { while (currentWeek.length < 7) { const lastDate = currentWeek[currentWeek.length - 1]?.date ?? - Temporal.PlainDate.from("2024-01-01"); - currentWeek.push(fillMissingDays ? { - date: lastDate.add({ days: 1 }), - events: [], - isToday: false, - isInCurrentPeriod: false, - } : null); + Temporal.PlainDate.from('2024-01-01') + currentWeek.push( + fillMissingDays + ? { + date: lastDate.add({ days: 1 }), + events: [], + isToday: false, + isInCurrentPeriod: false, + } + : null, + ) } - weeks.push(currentWeek); + weeks.push(currentWeek) } - - return weeks; + + return weeks } - default: break + default: + break } - return groups; + return groups }, [weekStartsOn], - ); - + ) return { ...state, From fe13c2c51b601735000c0e40000bb9b26322834d Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 11 Jun 2024 22:11:03 +0200 Subject: [PATCH 56/95] refactor: add the fillMissingDays argument --- docs/framework/react/reference/useCalendar.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 4984687..418bc27 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -68,8 +68,8 @@ export function useCalendar({ - This function is used to retrieve the style properties and current time for the current time marker. - `isPending: boolean` - This value represents whether the calendar is in a pending state. -- `groupDaysBy: (days: Day[], unit: 'day' | 'week' | 'month', fillMissingDays?: boolean) => Day[][]` - - This function is used to group the days in the current period by a specified unit. +- `groupDaysBy: ({ days: Day[], unit: 'week' | 'month', fillMissingDays?: boolean }) => Day[][]` + - This function is used to group the days in the current period by a specified unit. The `fillMissingDays` parameter can be used to fill in missing days with previous or next month's days. #### Example Usage From 9f998463e96541287245eaafbf55702cf5006a16 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 12 Jun 2024 00:57:15 +0200 Subject: [PATCH 57/95] refactor: move utilities to the core package --- docs/framework/react/reference/useCalendar.md | 4 +- .../react-time/src/tests/useCalendar.test.tsx | 4 +- .../react-time/src/useCalendar/useCalendar.ts | 260 +----------------- packages/time/package.json | 3 + .../time/src/calendar/generateDateRange.ts | 14 + .../src/calendar/getCurrentTimeMarkerProps.ts | 17 ++ packages/time/src/calendar/getEventProps.ts | 78 ++++++ packages/time/src/calendar/groupDaysBy.ts | 97 +++++++ packages/time/src/calendar/index.ts | 5 + .../time/src/calendar/splitMultiDayEvents.ts | 23 ++ packages/time/src/calendar/types.ts | 24 ++ packages/time/src/index.ts | 3 +- pnpm-lock.yaml | 4 + 13 files changed, 286 insertions(+), 250 deletions(-) create mode 100644 packages/time/src/calendar/generateDateRange.ts create mode 100644 packages/time/src/calendar/getCurrentTimeMarkerProps.ts create mode 100644 packages/time/src/calendar/getEventProps.ts create mode 100644 packages/time/src/calendar/groupDaysBy.ts create mode 100644 packages/time/src/calendar/index.ts create mode 100644 packages/time/src/calendar/splitMultiDayEvents.ts create mode 100644 packages/time/src/calendar/types.ts diff --git a/docs/framework/react/reference/useCalendar.md b/docs/framework/react/reference/useCalendar.md index 418bc27..0582268 100644 --- a/docs/framework/react/reference/useCalendar.md +++ b/docs/framework/react/reference/useCalendar.md @@ -64,7 +64,7 @@ export function useCalendar({ - This function is used to retrieve the style properties for a specific event based on its ID. - `getEventProps: (id: string) => { style: CSSProperties } | null` - This function is used to retrieve the style properties for a specific event based on its ID. -- `currentTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` +- `getCurrentTimeMarkerProps: () => { style: CSSProperties, currentTime: Temporal.PlainTime }` - This function is used to retrieve the style properties and current time for the current time marker. - `isPending: boolean` - This value represents whether the calendar is in a pending state. @@ -87,7 +87,7 @@ const CalendarComponent = ({ events }) => { daysNames, viewMode, getEventProps, - currentTimeMarkerProps, + getCurrentTimeMarkerProps, groupDaysBy, } = useCalendar({ weekStartsOn: 1, diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 68a1be7..d1f1405 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -163,9 +163,9 @@ describe('useCalendar', () => { useCalendar({ events, viewMode: { value: 1, unit: 'week' } }), ) - const currentTimeMarkerProps = result.current.currentTimeMarkerProps() + const getCurrentTimeMarkerProps = result.current.getCurrentTimeMarkerProps() - expect(currentTimeMarkerProps).toEqual({ + expect(getCurrentTimeMarkerProps).toEqual({ style: { position: 'absolute', top: '45.83333333333333%', diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 72c12c1..29adfe1 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,23 +1,18 @@ import { useCallback, useEffect, useMemo, useTransition } from 'react' import { Temporal } from '@js-temporal/polyfill' -import { getFirstDayOfMonth, getFirstDayOfWeek } from '@tanstack/time' +import { generateDateRange, getEventProps, getFirstDayOfMonth, getFirstDayOfWeek, groupDaysBy, splitMultiDayEvents } from '@tanstack/time' import { actions } from './calendarActions' import { useCalendarReducer } from './useCalendarReducer' +import type { CalendarState, Event, GroupDaysByProps} from '@tanstack/time'; import type { UseCalendarAction } from './calendarActions' -import type { Event, UseCalendarState } from './useCalendarState' import type { CSSProperties } from 'react' -type Day = { - date: Temporal.PlainDate - events: TEvent[] - isToday: boolean - isInCurrentPeriod: boolean -} + interface UseCalendarProps< TEvent extends Event, - TState extends UseCalendarState = UseCalendarState, + TState extends CalendarState = CalendarState, > { /** * The day of the week the calendar should start on (0 for Sunday, 6 for Saturday). @@ -49,61 +44,6 @@ interface UseCalendarProps< ) => TState } -const splitMultiDayEvents = (event: TEvent): TEvent[] => { - const startDate = Temporal.PlainDateTime.from(event.startDate) - const endDate = Temporal.PlainDateTime.from(event.endDate) - const events: TEvent[] = [] - - let currentDay = startDate - while ( - Temporal.PlainDate.compare( - currentDay.toPlainDate(), - endDate.toPlainDate(), - ) < 0 - ) { - const startOfCurrentDay = currentDay.with({ - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }) - const endOfCurrentDay = currentDay.with({ - hour: 23, - minute: 59, - second: 59, - millisecond: 999, - }) - - const eventStart = - Temporal.PlainDateTime.compare(currentDay, startDate) === 0 - ? startDate - : startOfCurrentDay - const eventEnd = - Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 - ? endDate - : endOfCurrentDay - - events.push({ ...event, startDate: eventStart, endDate: eventEnd }) - - currentDay = startOfCurrentDay.add({ days: 1 }) - } - - return events -} - -const generateDateRange = ( - start: Temporal.PlainDate, - end: Temporal.PlainDate, -) => { - const dates: Temporal.PlainDate[] = [] - let current = start - while (Temporal.PlainDate.compare(current, end) <= 0) { - dates.push(current) - current = current.add({ days: 1 }) - } - return dates -} - /** * Hook to manage the state and behavior of a calendar. * @@ -130,7 +70,7 @@ const generateDateRange = ( */ export const useCalendar = < TEvent extends Event, - TState extends UseCalendarState = UseCalendarState, + TState extends CalendarState = CalendarState, >({ weekStartsOn = 1, events, @@ -220,7 +160,7 @@ export const useCalendar = < eventEndDate.toPlainDate(), ) !== 0 ) { - const splitEvents = splitMultiDayEvents(event) + const splitEvents = splitMultiDayEvents(event) splitEvents.forEach((splitEvent) => { const splitKey = splitEvent.startDate.toString().split('T')[0] if (splitKey) { @@ -298,90 +238,9 @@ export const useCalendar = < [dispatch, onChangeViewMode], ) - const getEventProps = useCallback( - (id: Event['id']): { style: CSSProperties } | null => { - const event = [...eventMap.values()] - .flat() - .find((currEvent) => currEvent.id === id) - if (!event) return null - - const eventStartDate = Temporal.PlainDateTime.from(event.startDate) - const eventEndDate = Temporal.PlainDateTime.from(event.endDate) - const isSplitEvent = - Temporal.PlainDate.compare( - eventStartDate.toPlainDate(), - eventEndDate.toPlainDate(), - ) !== 0 - - let percentageOfDay - let eventHeightInMinutes - - if (isSplitEvent) { - const isStartPart = - eventStartDate.hour !== 0 || eventStartDate.minute !== 0 - if (isStartPart) { - const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - eventHeightInMinutes = 24 * 60 - eventTimeInMinutes - } else { - percentageOfDay = 0 - eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute - } - } else { - const eventTimeInMinutes = - eventStartDate.hour * 60 + eventStartDate.minute - percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100 - const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute - eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes - } - - const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20) - - const overlappingEvents = [...eventMap.values()].flat().filter((e) => { - const eStartDate = Temporal.PlainDateTime.from(e.startDate) - const eEndDate = Temporal.PlainDateTime.from(e.endDate) - return ( - (e.id !== id && - Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && - Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && - Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && - Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || - (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && - Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) - ) - }) - - const eventIndex = overlappingEvents.findIndex((e) => e.id === id) - const totalOverlaps = overlappingEvents.length - const sidePadding = 2 - const innerPadding = 2 - const totalInnerPadding = (totalOverlaps - 1) * innerPadding - const availableWidth = 100 - totalInnerPadding - 2 * sidePadding - const eventWidth = - totalOverlaps > 0 - ? availableWidth / totalOverlaps - : 100 - 2 * sidePadding - const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding) - - if (state.viewMode.unit === 'week' || state.viewMode.unit === 'day') { - return { - style: { - position: 'absolute', - top: `min(${percentageOfDay}%, calc(100% - 55px))`, - left: `${eventLeft}%`, - width: `${eventWidth}%`, - margin: 0, - height: `${eventHeight}%`, - }, - } - } - - return null - }, - [eventMap, state.viewMode], + const getEventPropsCallback = useCallback( + (id: Event['id']): { style: CSSProperties } | null => getEventProps(eventMap, id, state), + [eventMap, state], ) useEffect(() => { @@ -393,7 +252,7 @@ export const useCalendar = < return () => clearInterval(intervalId) }, [dispatch]) - const currentTimeMarkerProps = useCallback(() => { + const getCurrentTimeMarkerProps = useCallback(() => { const { hour, minute } = state.currentTime const currentTimeInMinutes = hour * 60 + minute const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 @@ -417,101 +276,12 @@ export const useCalendar = < ) }, [locale, weekStartsOn]) - const groupDaysBy = useCallback( + const groupDaysByCallback = useCallback( ({ days, unit, fillMissingDays = true, - }: - | { - days: (Day | null)[] - unit: 'month' - fillMissingDays?: never - } - | { - days: (Day | null)[] - unit: 'week' - fillMissingDays?: boolean - }) => { - const groups: (Day | null)[][] = [] - - switch (unit) { - case 'month': { - let currentMonth: (Day | null)[] = [] - days.forEach((day) => { - if ( - currentMonth.length > 0 && - day?.date.month !== currentMonth[0]?.date.month - ) { - groups.push(currentMonth) - currentMonth = [] - } - currentMonth.push(day) - }) - if (currentMonth.length > 0) { - groups.push(currentMonth) - } - break - } - - case 'week': { - const weeks: (Day | null)[][] = [] - let currentWeek: (Day | null)[] = [] - - days.forEach((day) => { - if ( - currentWeek.length === 0 && - day?.date.dayOfWeek !== weekStartsOn - ) { - if (day) { - const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7 - for (let i = 0; i < dayOfWeek; i++) { - currentWeek.push( - fillMissingDays - ? { - date: day.date.subtract({ days: dayOfWeek - i }), - events: [], - isToday: false, - isInCurrentPeriod: false, - } - : null, - ) - } - } - } - currentWeek.push(day) - if (currentWeek.length === 7) { - weeks.push(currentWeek) - currentWeek = [] - } - }) - - if (currentWeek.length > 0) { - while (currentWeek.length < 7) { - const lastDate = - currentWeek[currentWeek.length - 1]?.date ?? - Temporal.PlainDate.from('2024-01-01') - currentWeek.push( - fillMissingDays - ? { - date: lastDate.add({ days: 1 }), - events: [], - isToday: false, - isInCurrentPeriod: false, - } - : null, - ) - } - weeks.push(currentWeek) - } - - return weeks - } - default: - break - } - return groups - }, + }: Omit, 'weekStartsOn'>) => groupDaysBy({ days, unit, fillMissingDays, weekStartsOn } as GroupDaysByProps), [weekStartsOn], ) @@ -524,9 +294,9 @@ export const useCalendar = < days: daysWithEvents, daysNames, changeViewMode, - getEventProps, - currentTimeMarkerProps, + getEventProps: getEventPropsCallback, + getCurrentTimeMarkerProps, isPending, - groupDaysBy, + groupDaysBy: groupDaysByCallback, } } diff --git a/packages/time/package.json b/packages/time/package.json index 1b37016..a123330 100644 --- a/packages/time/package.json +++ b/packages/time/package.json @@ -58,5 +58,8 @@ ], "dependencies": { "@js-temporal/polyfill": "^0.4.4" + }, + "devDependencies": { + "csstype": "^3.1.3" } } diff --git a/packages/time/src/calendar/generateDateRange.ts b/packages/time/src/calendar/generateDateRange.ts new file mode 100644 index 0000000..ec73960 --- /dev/null +++ b/packages/time/src/calendar/generateDateRange.ts @@ -0,0 +1,14 @@ +import { Temporal } from '@js-temporal/polyfill' + +export const generateDateRange = ( + start: Temporal.PlainDate, + end: Temporal.PlainDate, +): Temporal.PlainDate[] => { + const dates: Temporal.PlainDate[] = [] + let current = start + while (Temporal.PlainDate.compare(current, end) <= 0) { + dates.push(current) + current = current.add({ days: 1 }) + } + return dates +} diff --git a/packages/time/src/calendar/getCurrentTimeMarkerProps.ts b/packages/time/src/calendar/getCurrentTimeMarkerProps.ts new file mode 100644 index 0000000..76d17c9 --- /dev/null +++ b/packages/time/src/calendar/getCurrentTimeMarkerProps.ts @@ -0,0 +1,17 @@ +import type { Temporal } from "@js-temporal/polyfill"; +import type { Properties as CSSProperties } from "csstype"; + +export const getCurrentTimeMarkerProps = (currentTime: Temporal.PlainDateTime): { style: CSSProperties; currentTime: string | undefined } => { + const { hour, minute } = currentTime; + const currentTimeInMinutes = hour * 60 + minute; + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; + + return { + style: { + position: 'absolute', + top: `${percentageOfDay}%`, + left: 0, + }, + currentTime: currentTime.toString().split('T')[1]?.substring(0, 5), + }; +}; \ No newline at end of file diff --git a/packages/time/src/calendar/getEventProps.ts b/packages/time/src/calendar/getEventProps.ts new file mode 100644 index 0000000..023d59b --- /dev/null +++ b/packages/time/src/calendar/getEventProps.ts @@ -0,0 +1,78 @@ +import { Temporal } from "@js-temporal/polyfill"; +import type { Properties as CSSProperties } from "csstype"; +import type { CalendarState, Event } from "./types"; + +export const getEventProps = ( + eventMap: Map, + id: Event['id'], + state: CalendarState +): { style: CSSProperties } | null => { + const event = [...eventMap.values()].flat().find((currEvent) => currEvent.id === id); + if (!event) return null; + + const eventStartDate = Temporal.PlainDateTime.from(event.startDate); + const eventEndDate = Temporal.PlainDateTime.from(event.endDate); + const isSplitEvent = Temporal.PlainDate.compare(eventStartDate.toPlainDate(), eventEndDate.toPlainDate()) !== 0; + + let percentageOfDay; + let eventHeightInMinutes; + + if (isSplitEvent) { + const isStartPart = eventStartDate.hour !== 0 || eventStartDate.minute !== 0; + if (isStartPart) { + const eventTimeInMinutes = eventStartDate.hour * 60 + eventStartDate.minute; + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100; + eventHeightInMinutes = 24 * 60 - eventTimeInMinutes; + } else { + percentageOfDay = 0; + eventHeightInMinutes = eventEndDate.hour * 60 + eventEndDate.minute; + } + } else { + const eventTimeInMinutes = eventStartDate.hour * 60 + eventStartDate.minute; + percentageOfDay = (eventTimeInMinutes / (24 * 60)) * 100; + const endTimeInMinutes = eventEndDate.hour * 60 + eventEndDate.minute; + eventHeightInMinutes = endTimeInMinutes - eventTimeInMinutes; + } + + const eventHeight = Math.min((eventHeightInMinutes / (24 * 60)) * 100, 20); + + const overlappingEvents = [...eventMap.values()].flat().filter((e) => { + const eStartDate = Temporal.PlainDateTime.from(e.startDate); + const eEndDate = Temporal.PlainDateTime.from(e.endDate); + return ( + (e.id !== id && + Temporal.PlainDateTime.compare(eventStartDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventStartDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eventEndDate, eStartDate) >= 0 && + Temporal.PlainDateTime.compare(eventEndDate, eEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eStartDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eStartDate, eventEndDate) <= 0) || + (Temporal.PlainDateTime.compare(eEndDate, eventStartDate) >= 0 && + Temporal.PlainDateTime.compare(eEndDate, eventEndDate) <= 0) + ); + }); + + const eventIndex = overlappingEvents.findIndex((e) => e.id === id); + const totalOverlaps = overlappingEvents.length; + const sidePadding = 2; + const innerPadding = 2; + const totalInnerPadding = (totalOverlaps - 1) * innerPadding; + const availableWidth = 100 - totalInnerPadding - 2 * sidePadding; + const eventWidth = totalOverlaps > 0 ? availableWidth / totalOverlaps : 100 - 2 * sidePadding; + const eventLeft = sidePadding + eventIndex * (eventWidth + innerPadding); + + if (state.viewMode.unit === 'week' || state.viewMode.unit === 'day') { + return { + style: { + position: 'absolute', + top: `min(${percentageOfDay}%, calc(100% - 55px))`, + left: `${eventLeft}%`, + width: `${eventWidth}%`, + margin: 0, + height: `${eventHeight}%`, + }, + }; + } + + return null; +}; \ No newline at end of file diff --git a/packages/time/src/calendar/groupDaysBy.ts b/packages/time/src/calendar/groupDaysBy.ts new file mode 100644 index 0000000..8b19e98 --- /dev/null +++ b/packages/time/src/calendar/groupDaysBy.ts @@ -0,0 +1,97 @@ +import { Temporal } from "@js-temporal/polyfill"; +import type { Day, Event } from "./types"; + +interface GroupDaysByBaseProps { + days: (Day | null)[]; + weekStartsOn: number; +} + +type GroupDaysByMonthProps = GroupDaysByBaseProps & { + unit: 'month'; + fillMissingDays?: never; +}; + +type GroupDaysByWeekProps = GroupDaysByBaseProps & { + unit: 'week'; + fillMissingDays?: boolean; +}; + +export type GroupDaysByProps = GroupDaysByMonthProps | GroupDaysByWeekProps; + +export const groupDaysBy = ({ + days, + unit, + fillMissingDays = true, + weekStartsOn, +}: GroupDaysByProps): (Day | null)[][] => { + const groups: (Day | null)[][] = []; + + switch (unit) { + case 'month': { + let currentMonth: (Day | null)[] = []; + days.forEach((day) => { + if (currentMonth.length > 0 && day?.date.month !== currentMonth[0]?.date.month) { + groups.push(currentMonth); + currentMonth = []; + } + currentMonth.push(day); + }); + if (currentMonth.length > 0) { + groups.push(currentMonth); + } + break; + } + + case 'week': { + const weeks: (Day | null)[][] = []; + let currentWeek: (Day | null)[] = []; + + days.forEach((day) => { + if (currentWeek.length === 0 && day?.date.dayOfWeek !== weekStartsOn) { + if (day) { + const dayOfWeek = (day.date.dayOfWeek - weekStartsOn + 7) % 7; + for (let i = 0; i < dayOfWeek; i++) { + currentWeek.push( + fillMissingDays + ? { + date: day.date.subtract({ days: dayOfWeek - i }), + events: [], + isToday: false, + isInCurrentPeriod: false, + } + : null + ); + } + } + } + currentWeek.push(day); + if (currentWeek.length === 7) { + weeks.push(currentWeek); + currentWeek = []; + } + }); + + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + const lastDate = currentWeek[currentWeek.length - 1]?.date ?? Temporal.PlainDate.from('2024-01-01'); + currentWeek.push( + fillMissingDays + ? { + date: lastDate.add({ days: 1 }), + events: [], + isToday: false, + isInCurrentPeriod: false, + } + : null + ); + } + weeks.push(currentWeek); + } + + return weeks; + } + default: + break; + } + return groups; +}; diff --git a/packages/time/src/calendar/index.ts b/packages/time/src/calendar/index.ts new file mode 100644 index 0000000..a16de01 --- /dev/null +++ b/packages/time/src/calendar/index.ts @@ -0,0 +1,5 @@ +export * from './types' +export * from './splitMultiDayEvents' +export * from './generateDateRange' +export * from './getEventProps' +export * from './groupDaysBy' \ No newline at end of file diff --git a/packages/time/src/calendar/splitMultiDayEvents.ts b/packages/time/src/calendar/splitMultiDayEvents.ts new file mode 100644 index 0000000..ebb0c2a --- /dev/null +++ b/packages/time/src/calendar/splitMultiDayEvents.ts @@ -0,0 +1,23 @@ +import { Temporal } from '@js-temporal/polyfill'; +import type { Event } from './types'; + +export const splitMultiDayEvents = (event: TEvent): TEvent[] => { + const startDate = Temporal.PlainDateTime.from(event.startDate); + const endDate = Temporal.PlainDateTime.from(event.endDate); + const events: TEvent[] = []; + + let currentDay = startDate; + while (Temporal.PlainDate.compare(currentDay.toPlainDate(), endDate.toPlainDate()) < 0) { + const startOfCurrentDay = currentDay.with({ hour: 0, minute: 0, second: 0, millisecond: 0 }); + const endOfCurrentDay = currentDay.with({ hour: 23, minute: 59, second: 59, millisecond: 999 }); + + const eventStart = Temporal.PlainDateTime.compare(currentDay, startDate) === 0 ? startDate : startOfCurrentDay; + const eventEnd = Temporal.PlainDateTime.compare(endDate, endOfCurrentDay) <= 0 ? endDate : endOfCurrentDay; + + events.push({ ...event, startDate: eventStart, endDate: eventEnd }); + + currentDay = startOfCurrentDay.add({ days: 1 }); + } + + return events; +}; diff --git a/packages/time/src/calendar/types.ts b/packages/time/src/calendar/types.ts new file mode 100644 index 0000000..d8552b4 --- /dev/null +++ b/packages/time/src/calendar/types.ts @@ -0,0 +1,24 @@ +import type { Temporal } from "@js-temporal/polyfill" + +export interface Event { + id: string + startDate: Temporal.PlainDateTime + endDate: Temporal.PlainDateTime + title: string +} + +export interface CalendarState { + currentPeriod: Temporal.PlainDate + viewMode: { + value: number + unit: 'month' | 'week' | 'day' + } + currentTime: Temporal.PlainDateTime +} + +export type Day = { + date: Temporal.PlainDate + events: TEvent[] + isToday: boolean + isInCurrentPeriod: boolean +} \ No newline at end of file diff --git a/packages/time/src/index.ts b/packages/time/src/index.ts index 0e36b7b..6370bcc 100644 --- a/packages/time/src/index.ts +++ b/packages/time/src/index.ts @@ -1,4 +1,5 @@ /** * TanStack Time */ -export * from './utils'; \ No newline at end of file +export * from './utils'; +export * from './calendar'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e5ac1a..7de84db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,6 +194,10 @@ importers: '@js-temporal/polyfill': specifier: ^0.4.4 version: 0.4.4 + devDependencies: + csstype: + specifier: ^3.1.3 + version: 3.1.3 packages/vue-time: dependencies: From 93b519a92c37fee040d4204a62b2825d2dd8f9e0 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 12 Jun 2024 16:42:47 +0200 Subject: [PATCH 58/95] fix: getCurrentTimeMarkerProps --- .../react-time/src/tests/useCalendar.test.tsx | 76 ++++++++++++++++--- .../react-time/src/useCalendar/useCalendar.ts | 22 ++++-- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index d1f1405..2a7f6d3 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -1,18 +1,11 @@ import { Temporal } from '@js-temporal/polyfill' -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useCalendar } from '../useCalendar' import type { UseCalendarAction } from '../useCalendar/calendarActions'; import type { UseCalendarState } from '../useCalendar/useCalendarState'; describe('useCalendar', () => { - beforeEach(() => { - vi.setSystemTime(new Date('2024-06-01T11:00:00')); - }); - afterEach(() => { - vi.useRealTimers(); - }); - const events = [ { id: '1', @@ -159,11 +152,13 @@ describe('useCalendar', () => { }) test('should return the correct props for the current time marker', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-06-01T11:00:00')); const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'week' } }), - ) + useCalendar({ viewMode: { value: 1, unit: 'week' } }), + ); - const getCurrentTimeMarkerProps = result.current.getCurrentTimeMarkerProps() + const getCurrentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); expect(getCurrentTimeMarkerProps).toEqual({ style: { @@ -172,8 +167,65 @@ describe('useCalendar', () => { left: 0, }, currentTime: '11:00', + }); + }); + + test('should update the current time marker props after time passes', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-06-01T11:00:00')); + const { result } = renderHook(() => + useCalendar({ viewMode: { value: 1, unit: 'week' } }), + ); + + let currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.83333333333333%', + left: 0, + }, + currentTime: '11:00', + }); + + act(() => { + vi.useFakeTimers(); + vi.advanceTimersByTime(60000); + }); + + currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.90277777777778%', + left: 0, + }, + currentTime: '11:01', + }); + }); + + test('should update the current time marker props after time passes when the next minute is in less than a minute', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-06-01T11:00:55')); + + const { result } = renderHook(() => useCalendar({ viewMode: { value: 1, unit: 'week' } })); + + act(() => { + vi.advanceTimersByTime(5000); }) - }) + + const currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.90277777777778%', + left: 0, + }, + currentTime: '11:01', + }); + }); test('should render array of days', () => { const { result } = renderHook(() => diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 29adfe1..4341b1f 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -243,14 +243,22 @@ export const useCalendar = < [eventMap, state], ) + const updateCurrentTime = useCallback(() => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), [dispatch]) + useEffect(() => { - const intervalId = setInterval( - () => - dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), - 60000, - ) - return () => clearInterval(intervalId) - }, [dispatch]) + const now = Temporal.Now.plainDateTimeISO(); + const msToNextMinute = (60 - now.second) * 1000 - now.millisecond; + + const timeoutId = setTimeout(() => { + updateCurrentTime(); + const intervalId = setInterval(updateCurrentTime, 60000); + + return () => clearInterval(intervalId); + }, msToNextMinute); + + return () => clearTimeout(timeoutId); + }, [dispatch, updateCurrentTime]); + const getCurrentTimeMarkerProps = useCallback(() => { const { hour, minute } = state.currentTime From 87544e219a02265e141d3eab9060eb5c182b9764 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 12 Jun 2024 18:03:24 +0200 Subject: [PATCH 59/95] docs: update jsdocs --- .../react-time/src/useCalendar/useCalendar.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 4341b1f..d5b933d 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -57,16 +57,16 @@ interface UseCalendarProps< * * @returns {Object} calendarState - The state and functions for managing the calendar. * @returns {Temporal.PlainDate} calendarState.currentPeriod - The current period displayed by the calendar. - * @returns {Function} calendarState.goToPreviousPeriod - Function to navigate to the previous period. - * @returns {Function} calendarState.goToNextPeriod - Function to navigate to the next period. - * @returns {Function} calendarState.goToCurrentPeriod - Function to navigate to the current period. - * @returns {Function} calendarState.goToSpecificPeriod - Function to navigate to a specific period. - * @returns {Array>} calendarState.days - The calendar grid, where each cell contains the date and events for that day. - * @returns {string[]} calendarState.daysNames - An array of day names based on the locale and week start day. * @returns {'month' | 'week' | number} calendarState.viewMode - The current view mode of the calendar. - * @returns {Function} calendarState.changeViewMode - Function to change the view mode of the calendar. - * @returns {Function} calendarState.getEventProps - Function to retrieve the style properties for a specific event based on its ID. - * @returns {Function} calendarState.currentTimeMarkerProps - Function to retrieve the style properties and current time for the current time marker. + * @returns {Function} goToPreviousPeriod - Function to navigate to the previous period. + * @returns {Function} goToNextPeriod - Function to navigate to the next period. + * @returns {Function} goToCurrentPeriod - Function to navigate to the current period. + * @returns {Function} goToSpecificPeriod - Function to navigate to a specific period. + * @returns {Array>} days - The calendar grid, where each cell contains the date and events for that day. + * @returns {string[]} daysNames - An array of day names based on the locale and week start day. + * @returns {Function} changeViewMode - Function to change the view mode of the calendar. + * @returns {Function} getEventProps - Function to retrieve the style properties for a specific event based on its ID. + * @returns {Function} currentTimeMarkerProps - Function to retrieve the style properties and current time for the current time marker. */ export const useCalendar = < TEvent extends Event, From b5ae21e1401eed1e628a34904acb237b8dfaa69b Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 12 Jun 2024 22:09:25 +0200 Subject: [PATCH 60/95] test: useCalendar --- .../react-time/src/tests/useCalendar.test.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 2a7f6d3..7bfe44b 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -1,6 +1,6 @@ import { Temporal } from '@js-temporal/polyfill' import { describe, expect, test, vi } from 'vitest' -import { act, renderHook } from '@testing-library/react' +import { act, renderHook, waitFor } from '@testing-library/react' import { useCalendar } from '../useCalendar' import type { UseCalendarAction } from '../useCalendar/calendarActions'; import type { UseCalendarState } from '../useCalendar/useCalendarState'; @@ -177,7 +177,7 @@ describe('useCalendar', () => { useCalendar({ viewMode: { value: 1, unit: 'week' } }), ); - let currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); + const currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); expect(currentTimeMarkerProps).toEqual({ style: { @@ -189,19 +189,18 @@ describe('useCalendar', () => { }); act(() => { - vi.useFakeTimers(); vi.advanceTimersByTime(60000); }); - currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); - - expect(currentTimeMarkerProps).toEqual({ - style: { - position: 'absolute', - top: '45.90277777777778%', - left: 0, - }, - currentTime: '11:01', + waitFor(() => { + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.90277777777778%', + left: 0, + }, + currentTime: '11:01', + }); }); }); From f5bd2b7cc6a1e78430e2ce05744ed1e3ac103d6b Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Wed, 12 Jun 2024 22:16:59 +0200 Subject: [PATCH 61/95] refactor: getCurrentTimeMarkerProps --- .../react-time/src/useCalendar/useCalendar.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index d5b933d..aa19d16 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useTransition } from 'react' +import { useCallback, useEffect, useMemo, useRef, useTransition } from 'react' import { Temporal } from '@js-temporal/polyfill' import { generateDateRange, getEventProps, getFirstDayOfMonth, getFirstDayOfWeek, groupDaysBy, splitMultiDayEvents } from '@tanstack/time' @@ -81,6 +81,7 @@ export const useCalendar = < }: UseCalendarProps) => { const today = Temporal.Now.plainDateISO() const [isPending, startTransition] = useTransition() + const currentTimeInterval = useRef() const [state, dispatch] = useCalendarReducer( { currentPeriod: today, @@ -246,17 +247,17 @@ export const useCalendar = < const updateCurrentTime = useCallback(() => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), [dispatch]) useEffect(() => { + if (currentTimeInterval.current) clearTimeout(currentTimeInterval.current); + const now = Temporal.Now.plainDateTimeISO(); const msToNextMinute = (60 - now.second) * 1000 - now.millisecond; - - const timeoutId = setTimeout(() => { + + currentTimeInterval.current = setTimeout(() => { updateCurrentTime(); - const intervalId = setInterval(updateCurrentTime, 60000); - - return () => clearInterval(intervalId); + currentTimeInterval.current = setInterval(updateCurrentTime, 60000); }, msToNextMinute); - - return () => clearTimeout(timeoutId); + + return () => clearTimeout(currentTimeInterval.current); }, [dispatch, updateCurrentTime]); From c03128c56688e6405e36d3d84d34c87667a987ad Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 13 Jun 2024 22:32:43 +0200 Subject: [PATCH 62/95] feat: calendar core --- .../react-time/src/useCalendar/useCalendar.ts | 343 +++--------------- packages/time/package.json | 3 +- packages/time/src/calendar-core.ts | 281 ++++++++++++++ packages/time/src/index.ts | 2 +- pnpm-lock.yaml | 7 + 5 files changed, 348 insertions(+), 288 deletions(-) create mode 100644 packages/time/src/calendar-core.ts diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index aa19d16..5d8acf3 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,252 +1,22 @@ -import { useCallback, useEffect, useMemo, useRef, useTransition } from 'react' -import { Temporal } from '@js-temporal/polyfill' +import { useEffect, useRef, useState, useTransition } from 'react'; +import { Temporal } from '@js-temporal/polyfill'; -import { generateDateRange, getEventProps, getFirstDayOfMonth, getFirstDayOfWeek, groupDaysBy, splitMultiDayEvents } from '@tanstack/time' -import { actions } from './calendarActions' -import { useCalendarReducer } from './useCalendarReducer' -import type { CalendarState, Event, GroupDaysByProps} from '@tanstack/time'; -import type { UseCalendarAction } from './calendarActions' -import type { CSSProperties } from 'react' +import { CalendarCore, type CalendarCoreOptions, type Event } from '@tanstack/time'; +import type { CalendarState} from '@tanstack/time'; +export const useCalendar = (options: CalendarCoreOptions) => { + const [calendarCore] = useState(() => new CalendarCore(options)); + const [state, setState] = useState(calendarCore.store.state); - -interface UseCalendarProps< - TEvent extends Event, - TState extends CalendarState = CalendarState, -> { - /** - * The day of the week the calendar should start on (0 for Sunday, 6 for Saturday). - * @default 1 - */ - weekStartsOn?: number - /** - * An array of events that the calendar should display. - */ - events?: TEvent[] - /** - * The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. - */ - viewMode: TState['viewMode'] - /** - * The locale to use for formatting dates and times. - */ - locale?: Parameters['0'] - /** - * Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. - */ - onChangeViewMode?: (viewMode: TState['viewMode']) => void - /** - * Custom reducer function to manage the state of the calendar. - */ - reducer?: ( - state: TState, - action: TAction, - ) => TState -} - -/** - * Hook to manage the state and behavior of a calendar. - * - * @param {UseCalendarProps} props - The configuration properties for the calendar. - * @param {number} [props.weekStartsOn=1] - The day of the week the calendar should start on (1 for Monday, 7 for Sunday). - * @param {TEvent[]} [props.events] - An array of events that the calendar should display. - * @param {'month' | 'week' | number} props.viewMode - The initial view mode of the calendar. It can be 'month', 'week', or a number representing the number of days in a custom view mode. - * @param {Intl.LocalesArgument} [props.locale] - The locale to use for formatting dates and times. - * @param {Function} [props.onChangeViewMode] - Callback function that is called when the view mode of the calendar changes. It receives the new view mode as an argument. - * @param {Function} [props.reducer] - Custom reducer function to manage the state of the calendar. - * - * @returns {Object} calendarState - The state and functions for managing the calendar. - * @returns {Temporal.PlainDate} calendarState.currentPeriod - The current period displayed by the calendar. - * @returns {'month' | 'week' | number} calendarState.viewMode - The current view mode of the calendar. - * @returns {Function} goToPreviousPeriod - Function to navigate to the previous period. - * @returns {Function} goToNextPeriod - Function to navigate to the next period. - * @returns {Function} goToCurrentPeriod - Function to navigate to the current period. - * @returns {Function} goToSpecificPeriod - Function to navigate to a specific period. - * @returns {Array>} days - The calendar grid, where each cell contains the date and events for that day. - * @returns {string[]} daysNames - An array of day names based on the locale and week start day. - * @returns {Function} changeViewMode - Function to change the view mode of the calendar. - * @returns {Function} getEventProps - Function to retrieve the style properties for a specific event based on its ID. - * @returns {Function} currentTimeMarkerProps - Function to retrieve the style properties and current time for the current time marker. - */ -export const useCalendar = < - TEvent extends Event, - TState extends CalendarState = CalendarState, ->({ - weekStartsOn = 1, - events, - viewMode: initialViewMode, - locale, - onChangeViewMode, - reducer, -}: UseCalendarProps) => { - const today = Temporal.Now.plainDateISO() - const [isPending, startTransition] = useTransition() - const currentTimeInterval = useRef() - const [state, dispatch] = useCalendarReducer( - { - currentPeriod: today, - viewMode: initialViewMode, - currentTime: Temporal.Now.plainDateTimeISO(), - } as TState, - reducer, - ) - - const firstDayOfMonth = useMemo( - () => - getFirstDayOfMonth( - state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7), - ), - [state.currentPeriod], - ) - - const firstDayOfWeek = useMemo( - () => getFirstDayOfWeek(state.currentPeriod.toString(), weekStartsOn), - [state.currentPeriod, weekStartsOn], - ) - - const calendarDays = useMemo(() => { - const start = - state.viewMode.unit === 'month' - ? firstDayOfMonth.subtract({ - days: (firstDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7, - }) - : state.currentPeriod - - let end - switch (state.viewMode.unit) { - case 'month': { - const lastDayOfMonth = firstDayOfMonth - .add({ months: state.viewMode.value }) - .subtract({ days: 1 }) - const lastDayOfMonthWeekDay = - (lastDayOfMonth.dayOfWeek - weekStartsOn + 7) % 7 - end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }) - break - } - case 'week': { - end = firstDayOfWeek.add({ days: 7 * state.viewMode.value - 1 }) - break - } - case 'day': { - end = state.currentPeriod.add({ days: state.viewMode.value - 1 }) - break - } - } - - const allDays = generateDateRange(start, end) - const startMonth = state.currentPeriod.month - const endMonth = state.currentPeriod.add({ - months: state.viewMode.value - 1, - }).month - - return allDays.filter( - (day) => day.month >= startMonth && day.month <= endMonth, - ) - }, [ - state.viewMode, - firstDayOfMonth, - firstDayOfWeek, - weekStartsOn, - state.currentPeriod, - ]) - - const eventMap = useMemo(() => { - const map = new Map() - events?.forEach((event) => { - const eventStartDate = Temporal.PlainDateTime.from(event.startDate) - const eventEndDate = Temporal.PlainDateTime.from(event.endDate) - if ( - Temporal.PlainDate.compare( - eventStartDate.toPlainDate(), - eventEndDate.toPlainDate(), - ) !== 0 - ) { - const splitEvents = splitMultiDayEvents(event) - splitEvents.forEach((splitEvent) => { - const splitKey = splitEvent.startDate.toString().split('T')[0] - if (splitKey) { - if (!map.has(splitKey)) map.set(splitKey, []) - map.get(splitKey)?.push(splitEvent) - } - }) - } else { - const eventKey = event.startDate.toString().split('T')[0] - if (eventKey) { - if (!map.has(eventKey)) map.set(eventKey, []) - map.get(eventKey)?.push(event) - } - } - }) - return map - }, [events]) - - const daysWithEvents = useMemo( - () => - calendarDays.map((day) => { - const dayKey = day.toString() - const dailyEvents = eventMap.get(dayKey) ?? [] - const currentMonthRange = Array.from( - { length: state.viewMode.value }, - (_, i) => state.currentPeriod.add({ months: i }).month, - ) - const isInCurrentPeriod = currentMonthRange.includes(day.month) - return { - date: day, - events: dailyEvents, - isToday: - Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, - isInCurrentPeriod, - } - }), - [calendarDays, eventMap, state.viewMode, state.currentPeriod], - ) - - const goToPreviousPeriod = useCallback( - () => - startTransition(() => - dispatch(actions.goToPreviousPeriod({ weekStartsOn })), - ), - [dispatch, weekStartsOn], - ) - - const goToNextPeriod = useCallback( - () => - startTransition(() => dispatch(actions.goToNextPeriod({ weekStartsOn }))), - [dispatch, weekStartsOn], - ) - - const goToCurrentPeriod = useCallback( - () => - startTransition(() => - dispatch(actions.setCurrentPeriod(Temporal.Now.plainDateISO())), - ), - [dispatch], - ) - - const goToSpecificPeriod = useCallback( - (date: Temporal.PlainDate) => - startTransition(() => dispatch(actions.setCurrentPeriod(date))), - [dispatch], - ) - - const changeViewMode = useCallback( - (newViewMode: TState['viewMode']) => { - startTransition(() => { - dispatch(actions.setViewMode(newViewMode)) - onChangeViewMode?.(newViewMode) - }) - }, - [dispatch, onChangeViewMode], - ) - - const getEventPropsCallback = useCallback( - (id: Event['id']): { style: CSSProperties } | null => getEventProps(eventMap, id, state), - [eventMap, state], - ) - - const updateCurrentTime = useCallback(() => dispatch(actions.updateCurrentTime(Temporal.Now.plainDateTimeISO())), [dispatch]) + const [isPending, startTransition] = useTransition(); + const currentTimeInterval = useRef(); useEffect(() => { + const updateCurrentTime = () => { + calendarCore.updateCurrentTime(); + setState({ ...calendarCore.store.state }); + }; + if (currentTimeInterval.current) clearTimeout(currentTimeInterval.current); const now = Temporal.Now.plainDateTimeISO(); @@ -258,41 +28,42 @@ export const useCalendar = < }, msToNextMinute); return () => clearTimeout(currentTimeInterval.current); - }, [dispatch, updateCurrentTime]); - - - const getCurrentTimeMarkerProps = useCallback(() => { - const { hour, minute } = state.currentTime - const currentTimeInMinutes = hour * 60 + minute - const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 - - return { - style: { - position: 'absolute', - top: `${percentageOfDay}%`, - left: 0, - }, - currentTime: state.currentTime.toString().split('T')[1]?.substring(0, 5), - } - }, [state.currentTime]) - - const daysNames = useMemo(() => { - const baseDate = Temporal.PlainDate.from('2024-01-01') - return Array.from({ length: 7 }).map((_, i) => - baseDate - .add({ days: (i + weekStartsOn - 1) % 7 }) - .toLocaleString(locale, { weekday: 'short' }), - ) - }, [locale, weekStartsOn]) - - const groupDaysByCallback = useCallback( - ({ - days, - unit, - fillMissingDays = true, - }: Omit, 'weekStartsOn'>) => groupDaysBy({ days, unit, fillMissingDays, weekStartsOn } as GroupDaysByProps), - [weekStartsOn], - ) + }, [calendarCore]); + + const goToPreviousPeriod = () => { + startTransition(() => { + calendarCore.goToPreviousPeriod(); + setState({ ...calendarCore.store.state }); + }); + }; + + const goToNextPeriod = () => { + startTransition(() => { + calendarCore.goToNextPeriod(); + setState({ ...calendarCore.store.state }); + }); + }; + + const goToCurrentPeriod = () => { + startTransition(() => { + calendarCore.goToCurrentPeriod(); + setState({ ...calendarCore.store.state }); + }); + }; + + const goToSpecificPeriod = (date: Temporal.PlainDate) => { + startTransition(() => { + calendarCore.goToSpecificPeriod(date); + setState({ ...calendarCore.store.state }); + }); + }; + + const changeViewMode = (newViewMode: CalendarState['viewMode']) => { + startTransition(() => { + calendarCore.changeViewMode(newViewMode); + setState({ ...calendarCore.store.state }); + }); + }; return { ...state, @@ -300,12 +71,12 @@ export const useCalendar = < goToNextPeriod, goToCurrentPeriod, goToSpecificPeriod, - days: daysWithEvents, - daysNames, + days: calendarCore.getDaysWithEvents(), + daysNames: calendarCore.getDaysNames(), changeViewMode, - getEventProps: getEventPropsCallback, - getCurrentTimeMarkerProps, + getEventProps: calendarCore.getEventProps.bind(calendarCore), + getCurrentTimeMarkerProps: calendarCore.getCurrentTimeMarkerProps.bind(calendarCore), isPending, - groupDaysBy: groupDaysByCallback, - } -} + groupDaysBy: calendarCore.groupDaysBy.bind(calendarCore), + }; +}; diff --git a/packages/time/package.json b/packages/time/package.json index a123330..44f5812 100644 --- a/packages/time/package.json +++ b/packages/time/package.json @@ -57,7 +57,8 @@ "src" ], "dependencies": { - "@js-temporal/polyfill": "^0.4.4" + "@js-temporal/polyfill": "^0.4.4", + "@tanstack/store": "^0.4.1" }, "devDependencies": { "csstype": "^3.1.3" diff --git a/packages/time/src/calendar-core.ts b/packages/time/src/calendar-core.ts new file mode 100644 index 0000000..f5ff903 --- /dev/null +++ b/packages/time/src/calendar-core.ts @@ -0,0 +1,281 @@ +import { Store } from '@tanstack/store'; +import { Temporal } from '@js-temporal/polyfill'; +import { getFirstDayOfMonth, getFirstDayOfWeek } from './utils'; +import { generateDateRange } from './calendar/generateDateRange'; +import { splitMultiDayEvents } from './calendar/splitMultiDayEvents'; +import { getEventProps } from './calendar/getEventProps'; +import { groupDaysBy } from './calendar/groupDaysBy'; +import type { GroupDaysByProps} from './calendar/groupDaysBy'; + +export interface Event { + id: string + startDate: Temporal.PlainDateTime + endDate: Temporal.PlainDateTime + title: string +} + +export interface CalendarState { + currentPeriod: Temporal.PlainDate + viewMode: { + value: number + unit: 'month' | 'week' | 'day' + } + currentTime: Temporal.PlainDateTime +} + +export type Day = { + date: Temporal.PlainDate + events: TEvent[] + isToday: boolean + isInCurrentPeriod: boolean +} + +export interface CalendarCoreOptions { + weekStartsOn: number; + events?: TEvent[]; + viewMode: CalendarState['viewMode']; + locale?: Parameters['0']; +} + +export class CalendarCore { + store: Store; + options: CalendarCoreOptions; + + constructor(options: CalendarCoreOptions) { + this.options = options; + this.store = new Store({ + currentPeriod: Temporal.Now.plainDateISO(), + viewMode: options.viewMode, + currentTime: Temporal.Now.plainDateTimeISO(), + }); + } + + private getFirstDayOfMonth() { + return getFirstDayOfMonth( + this.store.state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7), + ); + } + + private getFirstDayOfWeek() { + return getFirstDayOfWeek(this.store.state.currentPeriod.toString(), this.options.weekStartsOn || 1); + } + + private getCalendarDays() { + const start = + this.store.state.viewMode.unit === 'month' + ? this.getFirstDayOfMonth().subtract({ + days: (this.getFirstDayOfMonth().dayOfWeek - (this.options.weekStartsOn || 1) + 7) % 7, + }) + : this.store.state.currentPeriod; + + let end; + switch (this.store.state.viewMode.unit) { + case 'month': { + const lastDayOfMonth = this.getFirstDayOfMonth() + .add({ months: this.store.state.viewMode.value }) + .subtract({ days: 1 }); + const lastDayOfMonthWeekDay = + (lastDayOfMonth.dayOfWeek - (this.options.weekStartsOn || 1) + 7) % 7; + end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }); + break; + } + case 'week': { + end = this.getFirstDayOfWeek().add({ days: 7 * this.store.state.viewMode.value - 1 }); + break; + } + case 'day': { + end = this.store.state.currentPeriod.add({ days: this.store.state.viewMode.value - 1 }); + break; + } + } + + const allDays = generateDateRange(start, end); + const startMonth = this.store.state.currentPeriod.month; + const endMonth = this.store.state.currentPeriod.add({ + months: this.store.state.viewMode.value - 1, + }).month; + + return allDays.filter( + (day) => day.month >= startMonth && day.month <= endMonth, + ); + } + + private getEventMap() { + const map = new Map(); + this.options.events?.forEach((event) => { + const eventStartDate = Temporal.PlainDateTime.from(event.startDate); + const eventEndDate = Temporal.PlainDateTime.from(event.endDate); + if ( + Temporal.PlainDate.compare( + eventStartDate.toPlainDate(), + eventEndDate.toPlainDate(), + ) !== 0 + ) { + const splitEvents = splitMultiDayEvents(event); + splitEvents.forEach((splitEvent) => { + const splitKey = splitEvent.startDate.toString().split('T')[0]; + if (splitKey) { + if (!map.has(splitKey)) map.set(splitKey, []); + map.get(splitKey)?.push(splitEvent); + } + }); + } else { + const eventKey = event.startDate.toString().split('T')[0]; + if (eventKey) { + if (!map.has(eventKey)) map.set(eventKey, []); + map.get(eventKey)?.push(event); + } + } + }); + return map; + } + + getDaysWithEvents() { + const calendarDays = this.getCalendarDays(); + const eventMap = this.getEventMap(); + return calendarDays.map((day) => { + const dayKey = day.toString(); + const dailyEvents = eventMap.get(dayKey) ?? []; + const currentMonthRange = Array.from( + { length: this.store.state.viewMode.value }, + (_, i) => this.store.state.currentPeriod.add({ months: i }).month, + ); + const isInCurrentPeriod = currentMonthRange.includes(day.month); + return { + date: day, + events: dailyEvents, + isToday: + Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, + isInCurrentPeriod, + }; + }); + } + + getDaysNames() { + const baseDate = Temporal.PlainDate.from('2024-01-01'); + return Array.from({ length: 7 }).map((_, i) => + baseDate + .add({ days: (i + (this.options.weekStartsOn || 1) - 1) % 7 }) + .toLocaleString(this.options.locale, { weekday: 'short' }), + ); + } + + changeViewMode(newViewMode: CalendarState['viewMode']) { + this.store.setState((prev) => ({ + ...prev, + viewMode: newViewMode, + })); + } + + goToPreviousPeriod() { + const firstDayOfMonth = this.getFirstDayOfMonth(); + const firstDayOfWeek = this.getFirstDayOfWeek(); + + switch (this.store.state.viewMode.unit) { + case 'month': { + const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: this.store.state.viewMode.value }); + this.store.setState((prev) => ({ + ...prev, + currentPeriod: firstDayOfPrevMonth, + })); + break; + } + + case 'week': { + const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: this.store.state.viewMode.value }); + this.store.setState((prev) => ({ + ...prev, + currentPeriod: firstDayOfPrevWeek, + })); + break; + } + + case 'day': { + const prevCustomStart = this.store.state.currentPeriod.subtract({ days: this.store.state.viewMode.value }); + this.store.setState((prev) => ({ + ...prev, + currentPeriod: prevCustomStart, + })); + break; + } + } + } + + goToNextPeriod() { + const firstDayOfMonth = this.getFirstDayOfMonth(); + const firstDayOfWeek = this.getFirstDayOfWeek(); + + switch (this.store.state.viewMode.unit) { + case 'month': { + const firstDayOfNextMonth = firstDayOfMonth.add({ months: this.store.state.viewMode.value }); + this.store.setState((prev) => ({ + ...prev, + currentPeriod: firstDayOfNextMonth, + })); + break; + } + + case 'week': { + const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: this.store.state.viewMode.value }); + this.store.setState((prev) => ({ + ...prev, + currentPeriod: firstDayOfNextWeek, + })); + break; + } + + case 'day': { + const nextCustomStart = this.store.state.currentPeriod.add({ days: this.store.state.viewMode.value }); + this.store.setState((prev) => ({ + ...prev, + currentPeriod: nextCustomStart, + })); + break; + } + } + } + + goToCurrentPeriod() { + this.store.setState((prev) => ({ + ...prev, + currentPeriod: Temporal.Now.plainDateISO(), + })); + } + + goToSpecificPeriod(date: Temporal.PlainDate) { + this.store.setState((prev) => ({ + ...prev, + currentPeriod: date, + })); + } + + updateCurrentTime() { + this.store.setState((prev) => ({ + ...prev, + currentTime: Temporal.Now.plainDateTimeISO(), + })); + } + + getEventProps(id: Event['id']) { + return getEventProps(this.getEventMap(), id, this.store.state); + } + + getCurrentTimeMarkerProps() { + const { hour, minute } = this.store.state.currentTime; + const currentTimeInMinutes = hour * 60 + minute; + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; + + return { + style: { + position: 'absolute', + top: `${percentageOfDay}%`, + left: 0, + }, + currentTime: this.store.state.currentTime.toString().split('T')[1]?.substring(0, 5), + }; + } + + groupDaysBy({ days, unit, fillMissingDays = true }: Omit, 'weekStartsOn'>) { + return groupDaysBy({ days, unit, fillMissingDays, weekStartsOn: this.options.weekStartsOn } as GroupDaysByProps); + } +} diff --git a/packages/time/src/index.ts b/packages/time/src/index.ts index 6370bcc..0e2559b 100644 --- a/packages/time/src/index.ts +++ b/packages/time/src/index.ts @@ -2,4 +2,4 @@ * TanStack Time */ export * from './utils'; -export * from './calendar'; +export * from './calendar-core'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7de84db..8e864d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,6 +194,9 @@ importers: '@js-temporal/polyfill': specifier: ^0.4.4 version: 0.4.4 + '@tanstack/store': + specifier: ^0.4.1 + version: 0.4.1 devDependencies: csstype: specifier: ^3.1.3 @@ -3310,6 +3313,10 @@ packages: - vite dev: true + /@tanstack/store@0.4.1: + resolution: {integrity: sha512-NvW3MomYSTzQK61AWdtWNIhWgszXFZDRgCNlvSDw/DBaoLqJIlZ0/gKLsditA8un/BGU1NR06+j0a/UNLgXA+Q==} + dev: false + /@testing-library/dom@9.3.4: resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} From 3c48a5e5972a8dedf48556bc145a870ec743ed6e Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Fri, 14 Jun 2024 00:10:37 +0200 Subject: [PATCH 63/95] feat: calendar core --- packages/time/src/calendar-core.ts | 61 +++--- .../src/calendar/getCurrentTimeMarkerProps.ts | 17 -- packages/time/src/calendar/types.ts | 8 +- packages/time/src/tests/calendar-core.test.ts | 188 ++++++++++++++++++ 4 files changed, 225 insertions(+), 49 deletions(-) delete mode 100644 packages/time/src/calendar/getCurrentTimeMarkerProps.ts create mode 100644 packages/time/src/tests/calendar-core.test.ts diff --git a/packages/time/src/calendar-core.ts b/packages/time/src/calendar-core.ts index f5ff903..4f2ed91 100644 --- a/packages/time/src/calendar-core.ts +++ b/packages/time/src/calendar-core.ts @@ -5,38 +5,43 @@ import { generateDateRange } from './calendar/generateDateRange'; import { splitMultiDayEvents } from './calendar/splitMultiDayEvents'; import { getEventProps } from './calendar/getEventProps'; import { groupDaysBy } from './calendar/groupDaysBy'; -import type { GroupDaysByProps} from './calendar/groupDaysBy'; +import type { Properties as CSSProperties } from 'csstype'; +import type { GroupDaysByProps } from './calendar/groupDaysBy'; +import type { CalendarState, Day, Event } from './calendar/types'; -export interface Event { - id: string - startDate: Temporal.PlainDateTime - endDate: Temporal.PlainDateTime - title: string -} - -export interface CalendarState { - currentPeriod: Temporal.PlainDate - viewMode: { - value: number - unit: 'month' | 'week' | 'day' - } - currentTime: Temporal.PlainDateTime -} +export type { CalendarState, Event, Day } from './calendar/types'; -export type Day = { - date: Temporal.PlainDate - events: TEvent[] - isToday: boolean - isInCurrentPeriod: boolean +export interface ViewMode { + value: number; + unit: 'month' | 'week' | 'day'; } export interface CalendarCoreOptions { - weekStartsOn: number; + weekStartsOn?: number; events?: TEvent[]; viewMode: CalendarState['viewMode']; locale?: Parameters['0']; } +export interface CalendarApi { + currentPeriod: CalendarState['currentPeriod']; + viewMode: CalendarState['viewMode']; + currentTime: CalendarState['currentTime']; + days: Array>; + daysNames: string[]; + goToPreviousPeriod: () => void; + goToNextPeriod: () => void; + goToCurrentPeriod: () => void; + goToSpecificPeriod: (date: Temporal.PlainDate) => void; + changeViewMode: (newViewMode: CalendarState['viewMode']) => void; + getEventProps: (id: Event['id']) => { style: CSSProperties } | null; + getCurrentTimeMarkerProps: () => { + style: CSSProperties; + currentTime: string | undefined; + }; + groupDaysBy: (props: Omit, 'weekStartsOn'>) => (Day | null)[][]; +} + export class CalendarCore { store: Store; options: CalendarCoreOptions; @@ -57,14 +62,14 @@ export class CalendarCore { } private getFirstDayOfWeek() { - return getFirstDayOfWeek(this.store.state.currentPeriod.toString(), this.options.weekStartsOn || 1); + return getFirstDayOfWeek(this.store.state.currentPeriod.toString(), this.options.weekStartsOn ?? 1); } private getCalendarDays() { const start = this.store.state.viewMode.unit === 'month' ? this.getFirstDayOfMonth().subtract({ - days: (this.getFirstDayOfMonth().dayOfWeek - (this.options.weekStartsOn || 1) + 7) % 7, + days: (this.getFirstDayOfMonth().dayOfWeek - (this.options.weekStartsOn ?? 1) + 7) % 7, }) : this.store.state.currentPeriod; @@ -75,7 +80,7 @@ export class CalendarCore { .add({ months: this.store.state.viewMode.value }) .subtract({ days: 1 }); const lastDayOfMonthWeekDay = - (lastDayOfMonth.dayOfWeek - (this.options.weekStartsOn || 1) + 7) % 7; + (lastDayOfMonth.dayOfWeek - (this.options.weekStartsOn ?? 1) + 7) % 7; end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }); break; } @@ -155,7 +160,7 @@ export class CalendarCore { const baseDate = Temporal.PlainDate.from('2024-01-01'); return Array.from({ length: 7 }).map((_, i) => baseDate - .add({ days: (i + (this.options.weekStartsOn || 1) - 1) % 7 }) + .add({ days: (i + (this.options.weekStartsOn ?? 1) - 1) % 7 }) .toLocaleString(this.options.locale, { weekday: 'short' }), ); } @@ -260,7 +265,7 @@ export class CalendarCore { return getEventProps(this.getEventMap(), id, this.store.state); } - getCurrentTimeMarkerProps() { + getCurrentTimeMarkerProps(): { style: CSSProperties; currentTime: string | undefined } { const { hour, minute } = this.store.state.currentTime; const currentTimeInMinutes = hour * 60 + minute; const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; @@ -276,6 +281,6 @@ export class CalendarCore { } groupDaysBy({ days, unit, fillMissingDays = true }: Omit, 'weekStartsOn'>) { - return groupDaysBy({ days, unit, fillMissingDays, weekStartsOn: this.options.weekStartsOn } as GroupDaysByProps); + return groupDaysBy({ days, unit, fillMissingDays, weekStartsOn: this.options.weekStartsOn ?? 1 } as GroupDaysByProps); } } diff --git a/packages/time/src/calendar/getCurrentTimeMarkerProps.ts b/packages/time/src/calendar/getCurrentTimeMarkerProps.ts deleted file mode 100644 index 76d17c9..0000000 --- a/packages/time/src/calendar/getCurrentTimeMarkerProps.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Temporal } from "@js-temporal/polyfill"; -import type { Properties as CSSProperties } from "csstype"; - -export const getCurrentTimeMarkerProps = (currentTime: Temporal.PlainDateTime): { style: CSSProperties; currentTime: string | undefined } => { - const { hour, minute } = currentTime; - const currentTimeInMinutes = hour * 60 + minute; - const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; - - return { - style: { - position: 'absolute', - top: `${percentageOfDay}%`, - left: 0, - }, - currentTime: currentTime.toString().split('T')[1]?.substring(0, 5), - }; -}; \ No newline at end of file diff --git a/packages/time/src/calendar/types.ts b/packages/time/src/calendar/types.ts index d8552b4..8da9ad3 100644 --- a/packages/time/src/calendar/types.ts +++ b/packages/time/src/calendar/types.ts @@ -1,10 +1,10 @@ import type { Temporal } from "@js-temporal/polyfill" export interface Event { - id: string - startDate: Temporal.PlainDateTime - endDate: Temporal.PlainDateTime - title: string + id: string; + startDate: Temporal.PlainDateTime; + endDate: Temporal.PlainDateTime; + title: string; } export interface CalendarState { diff --git a/packages/time/src/tests/calendar-core.test.ts b/packages/time/src/tests/calendar-core.test.ts new file mode 100644 index 0000000..25c576c --- /dev/null +++ b/packages/time/src/tests/calendar-core.test.ts @@ -0,0 +1,188 @@ +import { Temporal } from '@js-temporal/polyfill'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { CalendarCore } from '../calendar-core'; +import type { CalendarCoreOptions, Event } from '../calendar-core'; + +describe('CalendarCore', () => { + let options: CalendarCoreOptions; + let calendarCore: CalendarCore; + const mockDate = Temporal.PlainDate.from('2023-06-15'); + const mockDateTime = Temporal.PlainDateTime.from('2023-06-15T10:00'); + + beforeEach(() => { + options = { + weekStartsOn: 1, + viewMode: { value: 1, unit: 'month' }, + events: [ + { + id: '1', + startDate: Temporal.PlainDateTime.from('2023-06-10T09:00'), + endDate: Temporal.PlainDateTime.from('2023-06-10T10:00'), + title: 'Event 1', + }, + { + id: '2', + startDate: Temporal.PlainDateTime.from('2023-06-12T11:00'), + endDate: Temporal.PlainDateTime.from('2023-06-12T12:00'), + title: 'Event 2', + }, + ], + }; + calendarCore = new CalendarCore(options); + vi.spyOn(Temporal.Now, 'plainDateISO').mockReturnValue(mockDate); + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(mockDateTime); + }); + + test('should initialize with the correct current period', () => { + const today = Temporal.Now.plainDateISO(); + expect(calendarCore.store.state.currentPeriod).toEqual(today); + }); + + test('should get the correct days with events for the month', () => { + const daysWithEvents = calendarCore.getDaysWithEvents(); + expect(daysWithEvents.length).toBeGreaterThan(0); + }); + + test('should correctly map events to days', () => { + const daysWithEvents = calendarCore.getDaysWithEvents(); + const dayWithEvent1 = daysWithEvents.find((day) => day.date.equals(Temporal.PlainDate.from('2023-06-10'))); + const dayWithEvent2 = daysWithEvents.find((day) => day.date.equals(Temporal.PlainDate.from('2023-06-12'))); + expect(dayWithEvent1?.events).toHaveLength(1); + expect(dayWithEvent1?.events[0]?.id).toBe('1'); + expect(dayWithEvent2?.events).toHaveLength(1); + expect(dayWithEvent2?.events[0]?.id).toBe('2'); + }); + + test('should change view mode correctly', () => { + calendarCore.changeViewMode({ value: 2, unit: 'week' }); + expect(calendarCore.store.state.viewMode.value).toBe(2); + expect(calendarCore.store.state.viewMode.unit).toBe('week'); + }); + + test('should go to previous period correctly', () => { + const initialPeriod = calendarCore.store.state.currentPeriod; + calendarCore.goToPreviousPeriod(); + const expectedPreviousMonth = initialPeriod.subtract({ months: 1 }); + expect(calendarCore.store.state.currentPeriod).toEqual(expectedPreviousMonth); + }); + + test('should go to next period correctly', () => { + const initialPeriod = calendarCore.store.state.currentPeriod; + calendarCore.goToNextPeriod(); + const expectedNextMonth = initialPeriod.add({ months: 1 }); + expect(calendarCore.store.state.currentPeriod).toEqual(expectedNextMonth); + }); + + test('should go to current period correctly', () => { + calendarCore.goToNextPeriod(); + calendarCore.goToCurrentPeriod(); + const today = Temporal.Now.plainDateISO(); + expect(calendarCore.store.state.currentPeriod).toEqual(today); + }); + + test('should go to specific period correctly', () => { + const specificDate = Temporal.PlainDate.from('2023-07-01'); + calendarCore.goToSpecificPeriod(specificDate); + expect(calendarCore.store.state.currentPeriod).toEqual(specificDate); + }); + + test('should update current time correctly', () => { + const initialTime = calendarCore.store.state.currentTime; + const newMockDateTime = initialTime.add({ minutes: 1 }); + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(newMockDateTime); + + calendarCore.updateCurrentTime(); + const updatedTime = calendarCore.store.state.currentTime; + expect(updatedTime).toEqual(newMockDateTime); + }); + + test('should return the correct props for the current time marker', () => { + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:00:00')); + + const coreOptions: CalendarCoreOptions = { + weekStartsOn: 1, + viewMode: { value: 1, unit: 'week' }, + events: [], + }; + calendarCore = new CalendarCore(coreOptions); + + const currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.83333333333333%', + left: 0, + }, + currentTime: '11:00', + }); + }); + + test('should update the current time marker props after time passes', () => { + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:00:00')); + + const coreOptions: CalendarCoreOptions = { + weekStartsOn: 1, + viewMode: { value: 1, unit: 'week' }, + events: [], + }; + calendarCore = new CalendarCore(coreOptions); + + let currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.83333333333333%', + left: 0, + }, + currentTime: '11:00', + }); + + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:01:00')); + + calendarCore.updateCurrentTime(); + currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.90277777777778%', + left: 0, + }, + currentTime: '11:01', + }); + }); + + test('should update the current time marker props after time passes when the next minute is in less than a minute', () => { + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:00:55')); + + + const coreOptions: CalendarCoreOptions = { + weekStartsOn: 1, + viewMode: { value: 1, unit: 'week' }, + events: [], + }; + calendarCore = new CalendarCore(coreOptions); + + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:01:00')); + + calendarCore.updateCurrentTime(); + const currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); + + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.90277777777778%', + left: 0, + }, + currentTime: '11:01', + }); + }); + + test('should group days correctly', () => { + const daysWithEvents = calendarCore.getDaysWithEvents(); + const groupedDays = calendarCore.groupDaysBy({ days: daysWithEvents, unit: 'month'}); + expect(groupedDays.length).toBeGreaterThan(0); + }); +}); From 649947544969204389342087fa119f36f8233be9 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Fri, 14 Jun 2024 00:10:44 +0200 Subject: [PATCH 64/95] feat: calendar core --- packages/react-time/package.json | 1 + .../react-time/src/tests/useCalendar.test.tsx | 26 ------ .../src/useCalendar/calendarActions.ts | 13 --- .../react-time/src/useCalendar/useCalendar.ts | 93 +++++++++---------- .../src/useCalendar/useCalendarReducer.ts | 90 ------------------ .../src/useCalendar/useCalendarState.ts | 17 ---- pnpm-lock.yaml | 15 +++ 7 files changed, 62 insertions(+), 193 deletions(-) delete mode 100644 packages/react-time/src/useCalendar/calendarActions.ts delete mode 100644 packages/react-time/src/useCalendar/useCalendarReducer.ts delete mode 100644 packages/react-time/src/useCalendar/useCalendarState.ts diff --git a/packages/react-time/package.json b/packages/react-time/package.json index 0181e8d..84a9a61 100644 --- a/packages/react-time/package.json +++ b/packages/react-time/package.json @@ -63,6 +63,7 @@ }, "dependencies": { "@js-temporal/polyfill": "^0.4.4", + "@tanstack/react-store": "^0.4.1", "@tanstack/time": "workspace:*", "typesafe-actions": "^5.1.0", "use-sync-external-store": "^1.2.0" diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index 7bfe44b..c59201b 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -2,8 +2,6 @@ import { Temporal } from '@js-temporal/polyfill' import { describe, expect, test, vi } from 'vitest' import { act, renderHook, waitFor } from '@testing-library/react' import { useCalendar } from '../useCalendar' -import type { UseCalendarAction } from '../useCalendar/calendarActions'; -import type { UseCalendarState } from '../useCalendar/useCalendarState'; describe('useCalendar', () => { const events = [ @@ -332,30 +330,6 @@ describe('useCalendar', () => { ) }) - test(`should allow overriding the reducer`, () => { - const customReducer = (state: UseCalendarState, action: UseCalendarAction) => { - if (action.type === 'SET_NEXT_PERIOD') { - return { - ...state, - currentPeriod: state.currentPeriod.add({ months: 2 }), - } - } - - return state - } - - const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'month' }, reducer: customReducer }), - ) - - act(() => { - result.current.goToNextPeriod() - }) - - const expectedNextMonth = Temporal.Now.plainDateISO().add({ months: 2 }) - expect(result.current.currentPeriod).toEqual(expectedNextMonth) - }); - test('should group days by months correctly', () => { const { result } = renderHook(() => useCalendar({ events, viewMode: { value: 2, unit: 'month' }, locale: 'en-US' }) diff --git a/packages/react-time/src/useCalendar/calendarActions.ts b/packages/react-time/src/useCalendar/calendarActions.ts deleted file mode 100644 index abf5c73..0000000 --- a/packages/react-time/src/useCalendar/calendarActions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createAction } from 'typesafe-actions'; -import type { Temporal } from '@js-temporal/polyfill'; -import type { ActionType } from 'typesafe-actions'; -import type { UseCalendarState } from './useCalendarState'; - -const setViewMode = createAction('SET_VIEW_MODE')(); -const updateCurrentTime = createAction('UPDATE_CURRENT_TIME')(); -const setCurrentPeriod = createAction('SET_CURRENT_PERIOD')(); -const goToNextPeriod = createAction('SET_NEXT_PERIOD')<{ weekStartsOn: number }>(); -const goToPreviousPeriod = createAction('SET_PREVIOUS_PERIOD')<{ weekStartsOn: number }>(); - -export const actions = { setCurrentPeriod, setViewMode, updateCurrentTime, goToNextPeriod, goToPreviousPeriod }; -export type UseCalendarAction = ActionType; diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 5d8acf3..f6b168c 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,82 +1,81 @@ -import { useEffect, useRef, useState, useTransition } from 'react'; -import { Temporal } from '@js-temporal/polyfill'; +import { useEffect, useRef, useState, useTransition } from 'react' +import { useStore } from '@tanstack/react-store' +import { Temporal } from '@js-temporal/polyfill' +import { CalendarCore, type CalendarState, type Event } from '@tanstack/time' +import type { CalendarApi, CalendarCoreOptions } from '@tanstack/time' -import { CalendarCore, type CalendarCoreOptions, type Event } from '@tanstack/time'; -import type { CalendarState} from '@tanstack/time'; +export const useCalendar = ( + options: CalendarCoreOptions, +): CalendarApi & { isPending: boolean } => { + const [calendarCore] = useState(() => new CalendarCore(options)) + const state = useStore(calendarCore.store) -export const useCalendar = (options: CalendarCoreOptions) => { - const [calendarCore] = useState(() => new CalendarCore(options)); - const [state, setState] = useState(calendarCore.store.state); - - const [isPending, startTransition] = useTransition(); - const currentTimeInterval = useRef(); + const [isPending, startTransition] = useTransition() + const currentTimeInterval = useRef() useEffect(() => { const updateCurrentTime = () => { - calendarCore.updateCurrentTime(); - setState({ ...calendarCore.store.state }); - }; + calendarCore.updateCurrentTime() + } - if (currentTimeInterval.current) clearTimeout(currentTimeInterval.current); + if (currentTimeInterval.current) clearTimeout(currentTimeInterval.current) - const now = Temporal.Now.plainDateTimeISO(); - const msToNextMinute = (60 - now.second) * 1000 - now.millisecond; + const now = Temporal.Now.plainDateTimeISO() + const msToNextMinute = (60 - now.second) * 1000 - now.millisecond currentTimeInterval.current = setTimeout(() => { - updateCurrentTime(); - currentTimeInterval.current = setInterval(updateCurrentTime, 60000); - }, msToNextMinute); + updateCurrentTime() + currentTimeInterval.current = setInterval(updateCurrentTime, 60000) + }, msToNextMinute) - return () => clearTimeout(currentTimeInterval.current); - }, [calendarCore]); + return () => clearTimeout(currentTimeInterval.current) + }, [calendarCore]) const goToPreviousPeriod = () => { startTransition(() => { - calendarCore.goToPreviousPeriod(); - setState({ ...calendarCore.store.state }); - }); - }; + calendarCore.goToPreviousPeriod() + }) + } const goToNextPeriod = () => { startTransition(() => { - calendarCore.goToNextPeriod(); - setState({ ...calendarCore.store.state }); - }); - }; + calendarCore.goToNextPeriod() + }) + } const goToCurrentPeriod = () => { startTransition(() => { - calendarCore.goToCurrentPeriod(); - setState({ ...calendarCore.store.state }); - }); - }; + calendarCore.goToCurrentPeriod() + }) + } const goToSpecificPeriod = (date: Temporal.PlainDate) => { startTransition(() => { - calendarCore.goToSpecificPeriod(date); - setState({ ...calendarCore.store.state }); - }); - }; + calendarCore.goToSpecificPeriod(date) + }) + } const changeViewMode = (newViewMode: CalendarState['viewMode']) => { startTransition(() => { - calendarCore.changeViewMode(newViewMode); - setState({ ...calendarCore.store.state }); - }); - }; + calendarCore.changeViewMode(newViewMode) + }) + } return { - ...state, + currentPeriod: state.currentPeriod, + viewMode: state.viewMode, + currentTime: state.currentTime, + days: calendarCore.getDaysWithEvents(), + daysNames: calendarCore.getDaysNames(), goToPreviousPeriod, goToNextPeriod, goToCurrentPeriod, goToSpecificPeriod, - days: calendarCore.getDaysWithEvents(), - daysNames: calendarCore.getDaysNames(), changeViewMode, getEventProps: calendarCore.getEventProps.bind(calendarCore), - getCurrentTimeMarkerProps: calendarCore.getCurrentTimeMarkerProps.bind(calendarCore), + getCurrentTimeMarkerProps: + calendarCore.getCurrentTimeMarkerProps.bind(calendarCore), isPending, groupDaysBy: calendarCore.groupDaysBy.bind(calendarCore), - }; -}; + } +} diff --git a/packages/react-time/src/useCalendar/useCalendarReducer.ts b/packages/react-time/src/useCalendar/useCalendarReducer.ts deleted file mode 100644 index d3b87f5..0000000 --- a/packages/react-time/src/useCalendar/useCalendarReducer.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { useMemo, useReducer } from 'react' -import { createReducer } from 'typesafe-actions' - -import { getFirstDayOfMonth, getFirstDayOfWeek } from '@tanstack/time' -import { type UseCalendarAction, actions } from './calendarActions' -import type { UseCalendarState } from './useCalendarState' - -const createCalendarReducer = (initialState: UseCalendarState) => { - return createReducer(initialState) - .handleAction(actions.setCurrentPeriod, (state, action) => ({ - ...state, - currentPeriod: action.payload, - })) - .handleAction(actions.setViewMode, (state, action) => ({ - ...state, - viewMode: action.payload, - })) - .handleAction(actions.updateCurrentTime, (state, action) => ({ - ...state, - currentTime: action.payload, - })) - .handleAction(actions.goToPreviousPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth(state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); - const firstDayOfWeek = getFirstDayOfWeek(state.currentPeriod.toString(), action.payload.weekStartsOn); - - switch (state.viewMode.unit) { - case 'month': { - const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: state.viewMode.value }); - return { - ...state, - currentPeriod: firstDayOfPrevMonth, - }; - } - case 'week': { - const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: state.viewMode.value }); - return { - ...state, - currentPeriod: firstDayOfPrevWeek, - }; - } - case 'day': { - const prevCustomStart = state.currentPeriod.subtract({ days: state.viewMode.value }); - return { - ...state, - currentPeriod: prevCustomStart, - }; - } - default: - return state; - } - }) - .handleAction(actions.goToNextPeriod, (state, action) => { - const firstDayOfMonth = getFirstDayOfMonth(state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7)); - const firstDayOfWeek = getFirstDayOfWeek(state.currentPeriod.toString(), action.payload.weekStartsOn); - - switch (state.viewMode.unit) { - case 'month': { - const firstDayOfNextMonth = firstDayOfMonth.add({ months: state.viewMode.value }); - return { - ...state, - currentPeriod: firstDayOfNextMonth, - }; - } - case 'week': { - const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: state.viewMode.value }); - return { - ...state, - currentPeriod: firstDayOfNextWeek, - }; - } - case 'day': { - const nextCustomStart = state.currentPeriod.add({ days: state.viewMode.value }); - return { - ...state, - currentPeriod: nextCustomStart, - }; - } - default: - return state; - } - }); -}; - -export const useCalendarReducer = ( - initialState: TState, - extReducer?: (state: TState, action: UseCalendarAction) => TState, -) => { - const reducer = useMemo(() => extReducer ?? createCalendarReducer(initialState), [extReducer, initialState]) - return useReducer(reducer, initialState) -} diff --git a/packages/react-time/src/useCalendar/useCalendarState.ts b/packages/react-time/src/useCalendar/useCalendarState.ts deleted file mode 100644 index c5a3b10..0000000 --- a/packages/react-time/src/useCalendar/useCalendarState.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Temporal } from '@js-temporal/polyfill' - -export interface Event { - id: string - startDate: Temporal.PlainDateTime - endDate: Temporal.PlainDateTime - title: string -} - -export interface UseCalendarState { - currentPeriod: Temporal.PlainDate - viewMode: { - value: number - unit: 'month' | 'week' | 'day' - } - currentTime: Temporal.PlainDateTime -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e864d4..626d76f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: '@js-temporal/polyfill': specifier: ^0.4.4 version: 0.4.4 + '@tanstack/react-store': + specifier: ^0.4.1 + version: 0.4.1(react-dom@18.2.0)(react@18.2.0) '@tanstack/time': specifier: workspace:* version: link:../time @@ -3313,6 +3316,18 @@ packages: - vite dev: true + /@tanstack/react-store@0.4.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cyriofh2I6dPOPJf2W0K+ON5q08ezevLTUhC1txiUnrJ9XFFFPr0X8CmGnXzucI2c0t0V6wYZc0GCz4zOAeptg==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/store': 0.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /@tanstack/store@0.4.1: resolution: {integrity: sha512-NvW3MomYSTzQK61AWdtWNIhWgszXFZDRgCNlvSDw/DBaoLqJIlZ0/gKLsditA8un/BGU1NR06+j0a/UNLgXA+Q==} dev: false From 2ac6065adaa2aa491ebee0504c3cdac0535e338f Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Fri, 14 Jun 2024 00:17:58 +0200 Subject: [PATCH 65/95] docs: calendar core --- docs/reference/calendar-core.md | 113 ++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/reference/calendar-core.md diff --git a/docs/reference/calendar-core.md b/docs/reference/calendar-core.md new file mode 100644 index 0000000..8c7b244 --- /dev/null +++ b/docs/reference/calendar-core.md @@ -0,0 +1,113 @@ +--- +title: Calendar Core +id: calendar-core +--- + +### `CalendarCore` + +```tsx +export class CalendarCore { + constructor(options: CalendarCoreOptions); +} +``` + +The `CalendarCore` class provides a set of functionalities for managing calendar events, view modes, and period navigation. This class is designed to be used in various calendar applications where precise date management and event handling are required. + + +#### Parameters + +- `weekStartsOn?: number` + - An optional number that specifies the day of the week that the calendar should start on. It defaults to 1 (Monday). +- `events?: TEvent[]` + - An optional array of events that the calendar should display. +- `viewMode: ViewMode` + - An object that specifies the initial view mode of the calendar. +- `locale?: Parameters['0']` + - An optional string that specifies the locale to use for formatting dates and times. + + +#### Returns + +- `getDaysWithEvents(): Array>` + - Returns an array of days in the current period with their associated events. +- `getDaysNames(): string[]` + - Returns an array of strings representing the names of the days of the week based on the locale and week start day. +- `changeViewMode(newViewMode: ViewMode): void` + - Changes the view mode of the calendar. +- `goToPreviousPeriod(): void` + - Navigates to the previous period based on the current view mode. +- `goToNextPeriod(): void` + - Navigates to the next period based on the current view mode. +- `goToCurrentPeriod(): void` + - Navigates to the current period. +- `goToSpecificPeriod(date: Temporal.PlainDate): void` + - Navigates to a specific period based on the provided date. +- `updateCurrentTime(): void` + - Updates the current time. +- `getEventProps(id: Event['id']): { style: CSSProperties } | null` + - Retrieves the style properties for a specific event based on its ID. +- `getCurrentTimeMarkerProps(): { style: CSSProperties; currentTime: string | undefined }` + - Retrieves the style properties and current time for the current time marker. +- `groupDaysBy(props: Omit, 'weekStartsOn'>): (Day | null)[][]` + - Groups the days in the current period by a specified unit. The fillMissingDays parameter can be used to fill in missing days with previous or next month's days. + + +#### Example Usage + +```ts +import { CalendarCore, Event } from 'your-calendar-core-package'; +import { Temporal } from '@js-temporal/polyfill'; + +interface MyEvent extends Event { + location: string; +} + +const events: MyEvent[] = [ + { + id: '1', + startDate: Temporal.PlainDateTime.from('2024-06-10T09:00'), + endDate: Temporal.PlainDateTime.from('2024-06-10T10:00'), + title: 'Event 1', + location: 'Room 101', + }, + { + id: '2', + startDate: Temporal.PlainDateTime.from('2024-06-12T11:00'), + endDate: Temporal.PlainDateTime.from('2024-06-12T12:00'), + title: 'Event 2', + location: 'Room 202', + }, +]; + +const calendarCore = new CalendarCore({ + weekStartsOn: 1, + viewMode: { value: 1, unit: 'month' }, + events, + locale: 'en-US', +}); + +// Get days with events +const daysWithEvents = calendarCore.getDaysWithEvents(); +console.log(daysWithEvents); + +// Change view mode to week +calendarCore.changeViewMode({ value: 1, unit: 'week' }); + +// Navigate to the next period +calendarCore.goToNextPeriod(); + +// Update current time +calendarCore.updateCurrentTime(); + +// Get event properties +const eventProps = calendarCore.getEventProps('1'); +console.log(eventProps); + +// Get current time marker properties +const currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); +console.log(currentTimeMarkerProps); + +// Group days by week +const groupedDays = calendarCore.groupDaysBy({ days: daysWithEvents, unit: 'week' }); +console.log(groupedDays); +``` From a71f5a83ae582053a8b97de11aab67f2fbfc06b3 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Fri, 14 Jun 2024 00:23:15 +0200 Subject: [PATCH 66/95] refactor: useCalendar --- .../react-time/src/tests/useCalendar.test.tsx | 21 ++++++++++--------- .../react-time/src/useCalendar/useCalendar.ts | 13 ++++++------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index c59201b..ea50fed 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -207,21 +207,22 @@ describe('useCalendar', () => { vi.setSystemTime(new Date('2024-06-01T11:00:55')); const { result } = renderHook(() => useCalendar({ viewMode: { value: 1, unit: 'week' } })); + const currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); act(() => { vi.advanceTimersByTime(5000); }) - const currentTimeMarkerProps = result.current.getCurrentTimeMarkerProps(); - - expect(currentTimeMarkerProps).toEqual({ - style: { - position: 'absolute', - top: '45.90277777777778%', - left: 0, - }, - currentTime: '11:01', - }); + waitFor(() => { + expect(currentTimeMarkerProps).toEqual({ + style: { + position: 'absolute', + top: '45.90277777777778%', + left: 0, + }, + currentTime: '11:01', + }); + }) }); test('should render array of days', () => { diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index f6b168c..6cbcdbf 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useTransition } from 'react' +import { useCallback, useEffect, useRef, useState, useTransition } from 'react' import { useStore } from '@tanstack/react-store' import { Temporal } from '@js-temporal/polyfill' import { CalendarCore, type CalendarState, type Event } from '@tanstack/time' @@ -9,15 +9,14 @@ export const useCalendar = ( ): CalendarApi & { isPending: boolean } => { const [calendarCore] = useState(() => new CalendarCore(options)) const state = useStore(calendarCore.store) - const [isPending, startTransition] = useTransition() const currentTimeInterval = useRef() - useEffect(() => { - const updateCurrentTime = () => { - calendarCore.updateCurrentTime() - } + const updateCurrentTime = useCallback(() => { + calendarCore.updateCurrentTime() + }, [calendarCore]) + useEffect(() => { if (currentTimeInterval.current) clearTimeout(currentTimeInterval.current) const now = Temporal.Now.plainDateTimeISO() @@ -29,7 +28,7 @@ export const useCalendar = ( }, msToNextMinute) return () => clearTimeout(currentTimeInterval.current) - }, [calendarCore]) + }, [calendarCore, updateCurrentTime]) const goToPreviousPeriod = () => { startTransition(() => { From 965de147280b039abb4147478ed9d7580e88c6c5 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 17 Jun 2024 16:45:53 +0200 Subject: [PATCH 67/95] test: calendar core --- packages/time/src/tests/calendar-core.test.ts | 67 ++----------------- 1 file changed, 7 insertions(+), 60 deletions(-) diff --git a/packages/time/src/tests/calendar-core.test.ts b/packages/time/src/tests/calendar-core.test.ts index 25c576c..74e6e34 100644 --- a/packages/time/src/tests/calendar-core.test.ts +++ b/packages/time/src/tests/calendar-core.test.ts @@ -118,67 +118,14 @@ describe('CalendarCore', () => { }); }); - test('should update the current time marker props after time passes', () => { - vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:00:00')); - - const coreOptions: CalendarCoreOptions = { - weekStartsOn: 1, - viewMode: { value: 1, unit: 'week' }, - events: [], - }; - calendarCore = new CalendarCore(coreOptions); - - let currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); - - expect(currentTimeMarkerProps).toEqual({ - style: { - position: 'absolute', - top: '45.83333333333333%', - left: 0, - }, - currentTime: '11:00', - }); - - vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:01:00')); - - calendarCore.updateCurrentTime(); - currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); - - expect(currentTimeMarkerProps).toEqual({ - style: { - position: 'absolute', - top: '45.90277777777778%', - left: 0, - }, - currentTime: '11:01', - }); - }); - - test('should update the current time marker props after time passes when the next minute is in less than a minute', () => { - vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:00:55')); - - - const coreOptions: CalendarCoreOptions = { - weekStartsOn: 1, - viewMode: { value: 1, unit: 'week' }, - events: [], - }; - calendarCore = new CalendarCore(coreOptions); - - vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:01:00')); - + test('should update the current time on', () => { + const initialTime = calendarCore.store.state.currentTime; + const newMockDateTime = initialTime.add({ minutes: 1 }); + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(newMockDateTime); + calendarCore.updateCurrentTime(); - const currentTimeMarkerProps = calendarCore.getCurrentTimeMarkerProps(); - - expect(currentTimeMarkerProps).toEqual({ - style: { - position: 'absolute', - top: '45.90277777777778%', - left: 0, - }, - currentTime: '11:01', - }); - }); + expect(initialTime).toEqual(newMockDateTime); + }) test('should group days correctly', () => { const daysWithEvents = calendarCore.getDaysWithEvents(); From 301d4b891bd04b99d953c6077cc288e4df5f70c5 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 17 Jun 2024 18:22:43 +0200 Subject: [PATCH 68/95] refactor: useCalendar --- .../react-time/src/useCalendar/useCalendar.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 6cbcdbf..1c87c74 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState, useTransition } from 'react' import { useStore } from '@tanstack/react-store' import { Temporal } from '@js-temporal/polyfill' import { CalendarCore, type CalendarState, type Event } from '@tanstack/time' -import type { CalendarApi, CalendarCoreOptions } from '@tanstack/time' +import type { CalendarApi, CalendarCoreOptions, GroupDaysByProps } from '@tanstack/time' export const useCalendar = ( options: CalendarCoreOptions, @@ -30,35 +30,41 @@ export const useCalendar = ( return () => clearTimeout(currentTimeInterval.current) }, [calendarCore, updateCurrentTime]) - const goToPreviousPeriod = () => { + const goToPreviousPeriod = useCallback(() => { startTransition(() => { calendarCore.goToPreviousPeriod() }) - } + }, [calendarCore, startTransition]) - const goToNextPeriod = () => { + const goToNextPeriod = useCallback(() => { startTransition(() => { calendarCore.goToNextPeriod() }) - } + }, [calendarCore, startTransition]) - const goToCurrentPeriod = () => { + const goToCurrentPeriod = useCallback(() => { startTransition(() => { calendarCore.goToCurrentPeriod() }) - } + }, [calendarCore, startTransition]) - const goToSpecificPeriod = (date: Temporal.PlainDate) => { + const goToSpecificPeriod = useCallback((date: Temporal.PlainDate) => { startTransition(() => { calendarCore.goToSpecificPeriod(date) }) - } + }, [calendarCore, startTransition]) - const changeViewMode = (newViewMode: CalendarState['viewMode']) => { + const changeViewMode = useCallback((newViewMode: CalendarState['viewMode']) => { startTransition(() => { calendarCore.changeViewMode(newViewMode) }) - } + }, [calendarCore, startTransition]) + + const getEventProps = useCallback((id: TEvent['id']) => calendarCore.getEventProps(id), [calendarCore]) + + const getCurrentTimeMarkerProps = useCallback(() => calendarCore.getCurrentTimeMarkerProps(), [calendarCore]) + + const groupDaysBy = useCallback((props: Omit, 'weekStartsOn'>) => calendarCore.groupDaysBy(props), [calendarCore]) return { currentPeriod: state.currentPeriod, @@ -71,10 +77,9 @@ export const useCalendar = ( goToCurrentPeriod, goToSpecificPeriod, changeViewMode, - getEventProps: calendarCore.getEventProps.bind(calendarCore), - getCurrentTimeMarkerProps: - calendarCore.getCurrentTimeMarkerProps.bind(calendarCore), + getEventProps, + getCurrentTimeMarkerProps, isPending, - groupDaysBy: calendarCore.groupDaysBy.bind(calendarCore), + groupDaysBy, } } From 1324311582943a61a57e8e56bf497cefeff00eda Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 17 Jun 2024 18:35:35 +0200 Subject: [PATCH 69/95] refactor: useCalendar --- packages/react-time/src/useCalendar/useCalendar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 1c87c74..ed0f428 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -64,7 +64,7 @@ export const useCalendar = ( const getCurrentTimeMarkerProps = useCallback(() => calendarCore.getCurrentTimeMarkerProps(), [calendarCore]) - const groupDaysBy = useCallback((props: Omit, 'weekStartsOn'>) => calendarCore.groupDaysBy(props), [calendarCore]) + const groupDaysBy = useCallback((props: Omit, "weekStartsOn">) => calendarCore.groupDaysBy(props), [calendarCore]) return { currentPeriod: state.currentPeriod, From 042bb13e11eef8d891edfeaf486f373c344e14b3 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 17 Jun 2024 18:35:42 +0200 Subject: [PATCH 70/95] refactor: useCalendar --- packages/time/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/time/src/index.ts b/packages/time/src/index.ts index 0e2559b..e2e01d1 100644 --- a/packages/time/src/index.ts +++ b/packages/time/src/index.ts @@ -3,3 +3,4 @@ */ export * from './utils'; export * from './calendar-core'; +export * from './calendar'; From 82c1c8776d4c076e4bb28cbcd49af0f9eec95914 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 17 Jun 2024 22:28:19 +0200 Subject: [PATCH 71/95] refactor(useCalendar): types --- .../react-time/src/useCalendar/useCalendar.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index ed0f428..6dd3412 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,8 +1,8 @@ import { useCallback, useEffect, useRef, useState, useTransition } from 'react' import { useStore } from '@tanstack/react-store' import { Temporal } from '@js-temporal/polyfill' -import { CalendarCore, type CalendarState, type Event } from '@tanstack/time' -import type { CalendarApi, CalendarCoreOptions, GroupDaysByProps } from '@tanstack/time' +import { CalendarCore, type Event } from '@tanstack/time' +import type { CalendarApi, CalendarCoreOptions } from '@tanstack/time' export const useCalendar = ( options: CalendarCoreOptions, @@ -12,7 +12,7 @@ export const useCalendar = ( const [isPending, startTransition] = useTransition() const currentTimeInterval = useRef() - const updateCurrentTime = useCallback(() => { + const updateCurrentTime = useCallback(() => { calendarCore.updateCurrentTime() }, [calendarCore]) @@ -30,41 +30,41 @@ export const useCalendar = ( return () => clearTimeout(currentTimeInterval.current) }, [calendarCore, updateCurrentTime]) - const goToPreviousPeriod = useCallback(() => { + const goToPreviousPeriod = useCallback(() => { startTransition(() => { calendarCore.goToPreviousPeriod() }) }, [calendarCore, startTransition]) - const goToNextPeriod = useCallback(() => { + const goToNextPeriod = useCallback(() => { startTransition(() => { calendarCore.goToNextPeriod() }) }, [calendarCore, startTransition]) - const goToCurrentPeriod = useCallback(() => { + const goToCurrentPeriod = useCallback(() => { startTransition(() => { calendarCore.goToCurrentPeriod() }) }, [calendarCore, startTransition]) - const goToSpecificPeriod = useCallback((date: Temporal.PlainDate) => { + const goToSpecificPeriod = useCallback((date) => { startTransition(() => { calendarCore.goToSpecificPeriod(date) }) }, [calendarCore, startTransition]) - const changeViewMode = useCallback((newViewMode: CalendarState['viewMode']) => { + const changeViewMode = useCallback((newViewMode) => { startTransition(() => { calendarCore.changeViewMode(newViewMode) }) }, [calendarCore, startTransition]) - const getEventProps = useCallback((id: TEvent['id']) => calendarCore.getEventProps(id), [calendarCore]) + const getEventProps = useCallback((id) => calendarCore.getEventProps(id), [calendarCore]) - const getCurrentTimeMarkerProps = useCallback(() => calendarCore.getCurrentTimeMarkerProps(), [calendarCore]) + const getCurrentTimeMarkerProps = useCallback(() => calendarCore.getCurrentTimeMarkerProps(), [calendarCore]) - const groupDaysBy = useCallback((props: Omit, "weekStartsOn">) => calendarCore.groupDaysBy(props), [calendarCore]) + const groupDaysBy = useCallback((props) => calendarCore.groupDaysBy(props), [calendarCore]) return { currentPeriod: state.currentPeriod, From f61a99d3a9b9f242cb3dab616895e7d8c75d3696 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:00:34 +0200 Subject: [PATCH 72/95] refactor: move calendar-related files to the core directory --- .../src/{calendar-core.ts => core/calendar.ts} | 16 ++++++++-------- packages/time/src/core/index.ts | 1 + packages/time/src/index.ts | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) rename packages/time/src/{calendar-core.ts => core/calendar.ts} (94%) create mode 100644 packages/time/src/core/index.ts diff --git a/packages/time/src/calendar-core.ts b/packages/time/src/core/calendar.ts similarity index 94% rename from packages/time/src/calendar-core.ts rename to packages/time/src/core/calendar.ts index 4f2ed91..b5ce5e5 100644 --- a/packages/time/src/calendar-core.ts +++ b/packages/time/src/core/calendar.ts @@ -1,15 +1,15 @@ import { Store } from '@tanstack/store'; import { Temporal } from '@js-temporal/polyfill'; -import { getFirstDayOfMonth, getFirstDayOfWeek } from './utils'; -import { generateDateRange } from './calendar/generateDateRange'; -import { splitMultiDayEvents } from './calendar/splitMultiDayEvents'; -import { getEventProps } from './calendar/getEventProps'; -import { groupDaysBy } from './calendar/groupDaysBy'; +import { getFirstDayOfMonth, getFirstDayOfWeek } from '../utils'; +import { generateDateRange } from '../calendar/generateDateRange'; +import { splitMultiDayEvents } from '../calendar/splitMultiDayEvents'; +import { getEventProps } from '../calendar/getEventProps'; +import { groupDaysBy } from '../calendar/groupDaysBy'; import type { Properties as CSSProperties } from 'csstype'; -import type { GroupDaysByProps } from './calendar/groupDaysBy'; -import type { CalendarState, Day, Event } from './calendar/types'; +import type { GroupDaysByProps } from '../calendar/groupDaysBy'; +import type { CalendarState, Day, Event } from '../calendar/types'; -export type { CalendarState, Event, Day } from './calendar/types'; +export type { CalendarState, Event, Day } from '../calendar/types'; export interface ViewMode { value: number; diff --git a/packages/time/src/core/index.ts b/packages/time/src/core/index.ts new file mode 100644 index 0000000..e00be3c --- /dev/null +++ b/packages/time/src/core/index.ts @@ -0,0 +1 @@ +export * from './calendar' diff --git a/packages/time/src/index.ts b/packages/time/src/index.ts index e2e01d1..0535016 100644 --- a/packages/time/src/index.ts +++ b/packages/time/src/index.ts @@ -2,5 +2,5 @@ * TanStack Time */ export * from './utils'; -export * from './calendar-core'; -export * from './calendar'; +export * from './core'; + From dc3bcbfee9cbc27f68ec04946af35d66bae7a826 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:15:32 +0200 Subject: [PATCH 73/95] refactor: update CalendarCoreOptions interface to use default generic type for TEvent --- packages/time/src/core/calendar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index b5ce5e5..4412972 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -16,7 +16,7 @@ export interface ViewMode { unit: 'month' | 'week' | 'day'; } -export interface CalendarCoreOptions { +export interface CalendarCoreOptions { weekStartsOn?: number; events?: TEvent[]; viewMode: CalendarState['viewMode']; From 1465e850ba989600f9630478d2d0779eba5e8bf4 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:16:25 +0200 Subject: [PATCH 74/95] refactor: Update CalendarCoreOptions interface in calendar.ts --- packages/time/src/core/calendar.ts | 2 +- packages/time/src/core/date-picker.ts | 64 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/time/src/core/date-picker.ts diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index b5ce5e5..4412972 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -16,7 +16,7 @@ export interface ViewMode { unit: 'month' | 'week' | 'day'; } -export interface CalendarCoreOptions { +export interface CalendarCoreOptions { weekStartsOn?: number; events?: TEvent[]; viewMode: CalendarState['viewMode']; diff --git a/packages/time/src/core/date-picker.ts b/packages/time/src/core/date-picker.ts new file mode 100644 index 0000000..30d96b7 --- /dev/null +++ b/packages/time/src/core/date-picker.ts @@ -0,0 +1,64 @@ +import { Store } from '@tanstack/store'; +import { Temporal } from '@js-temporal/polyfill'; +import { CalendarCore } from './calendar'; +import type { CalendarCoreOptions, CalendarState, Event } from './calendar'; + +export interface DatePickerOptions extends CalendarCoreOptions { + minDate?: Temporal.PlainDate; + maxDate?: Temporal.PlainDate; + onSelectDate?: (date: Temporal.PlainDate) => void; + multiple?: boolean; + range?: boolean; + selectedDates?: Temporal.PlainDate[]; +} + +export interface DatePickerState extends CalendarState { + selectedDates: Map; +} + +export class DatePicker extends CalendarCore { + datePickerStore: Store; + options: DatePickerOptions; + + constructor(options: DatePickerOptions) { + super(options); + this.options = options; + this.datePickerStore = new Store({ + ...this.store.state, + selectedDates: new Map(options.selectedDates?.map(date => [date.toString(), date]) ?? []), + }); + } + + getSelectedDates() { + return Array.from(this.datePickerStore.state.selectedDates.values()); + } + + selectDate(date: Temporal.PlainDate) { + const { multiple, range, minDate, maxDate, onSelectDate } = this.options; + + if (minDate && Temporal.PlainDate.compare(date, minDate) < 0) return; + if (maxDate && Temporal.PlainDate.compare(date, maxDate) > 0) return; + + const selectedDates = new Map(this.datePickerStore.state.selectedDates); + + if (range && selectedDates.size === 1) { + selectedDates.set(date.toString(), date); + } else if (multiple) { + if (selectedDates.has(date.toString())) { + selectedDates.delete(date.toString()); + } else { + selectedDates.set(date.toString(), date); + } + } else { + selectedDates.clear(); + selectedDates.set(date.toString(), date); + } + + this.datePickerStore.setState(prev => ({ + ...prev, + selectedDates, + })); + + onSelectDate?.(date); + } +} From 55751f829c0b6f2680952ab2cc8d000b17820a3e Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:26:48 +0200 Subject: [PATCH 75/95] feat: date picker core --- packages/time/src/core/calendar.ts | 2 +- packages/time/src/core/date-picker.ts | 64 +++++++++++++ .../time/src/tests/date-picker-core.test.ts | 92 +++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 packages/time/src/core/date-picker.ts create mode 100644 packages/time/src/tests/date-picker-core.test.ts diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 4412972..15ccb15 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -42,7 +42,7 @@ export interface CalendarApi { groupDaysBy: (props: Omit, 'weekStartsOn'>) => (Day | null)[][]; } -export class CalendarCore { +export class CalendarCore { store: Store; options: CalendarCoreOptions; diff --git a/packages/time/src/core/date-picker.ts b/packages/time/src/core/date-picker.ts new file mode 100644 index 0000000..005681e --- /dev/null +++ b/packages/time/src/core/date-picker.ts @@ -0,0 +1,64 @@ +import { Store } from '@tanstack/store'; +import { Temporal } from '@js-temporal/polyfill'; +import { CalendarCore } from './calendar'; +import type { CalendarCoreOptions, CalendarState } from './calendar'; + +export interface DatePickerOptions extends CalendarCoreOptions { + minDate?: Temporal.PlainDate; + maxDate?: Temporal.PlainDate; + onSelectDate?: (date: Temporal.PlainDate) => void; + multiple?: boolean; + range?: boolean; + selectedDates?: Temporal.PlainDate[]; +} + +export interface DatePickerState extends CalendarState { + selectedDates: Map; +} + +export class DatePicker extends CalendarCore { + datePickerStore: Store; + options: DatePickerOptions; + + constructor(options: DatePickerOptions) { + super(options); + this.options = options; + this.datePickerStore = new Store({ + ...this.store.state, + selectedDates: new Map(options.selectedDates?.map(date => [date.toString(), date]) ?? []), + }); + } + + getSelectedDates() { + return Array.from(this.datePickerStore.state.selectedDates.values()); + } + + selectDate(date: Temporal.PlainDate) { + const { multiple, range, minDate, maxDate, onSelectDate } = this.options; + + if (minDate && Temporal.PlainDate.compare(date, minDate) < 0) return; + if (maxDate && Temporal.PlainDate.compare(date, maxDate) > 0) return; + + const selectedDates = new Map(this.datePickerStore.state.selectedDates); + + if (range && selectedDates.size === 1) { + selectedDates.set(date.toString(), date); + } else if (multiple) { + if (selectedDates.has(date.toString())) { + selectedDates.delete(date.toString()); + } else { + selectedDates.set(date.toString(), date); + } + } else { + selectedDates.clear(); + selectedDates.set(date.toString(), date); + } + + this.datePickerStore.setState(prev => ({ + ...prev, + selectedDates, + })); + + onSelectDate?.(date); + } +} diff --git a/packages/time/src/tests/date-picker-core.test.ts b/packages/time/src/tests/date-picker-core.test.ts new file mode 100644 index 0000000..3e2a5f8 --- /dev/null +++ b/packages/time/src/tests/date-picker-core.test.ts @@ -0,0 +1,92 @@ +import { Temporal } from '@js-temporal/polyfill'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { DatePicker } from '../core/date-picker'; +import type { DatePickerOptions } from '../core/date-picker'; + +describe('DatePicker', () => { + let options: DatePickerOptions; + let datePicker: DatePicker; + const mockDate = Temporal.PlainDate.from('2023-06-15'); + const mockDateTime = Temporal.PlainDateTime.from('2023-06-15T10:00'); + + beforeEach(() => { + options = { + weekStartsOn: 1, + viewMode: { value: 1, unit: 'month' }, + events: [ + { + id: '1', + startDate: Temporal.PlainDateTime.from('2023-06-10T09:00'), + endDate: Temporal.PlainDateTime.from('2023-06-10T10:00'), + title: 'Event 1', + }, + { + id: '2', + startDate: Temporal.PlainDateTime.from('2023-06-12T11:00'), + endDate: Temporal.PlainDateTime.from('2023-06-12T12:00'), + title: 'Event 2', + }, + ], + selectedDates: [Temporal.PlainDate.from('2023-06-10')], + multiple: true, + }; + datePicker = new DatePicker(options); + vi.spyOn(Temporal.Now, 'plainDateISO').mockReturnValue(mockDate); + vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(mockDateTime); + }); + + test('should initialize with the correct selected dates', () => { + const selectedDates = datePicker.getSelectedDates(); + expect(selectedDates).toHaveLength(1); + expect(selectedDates[0]?.toString()).toBe('2023-06-10'); + }); + + test('should select a date correctly in single selection mode', () => { + datePicker.selectDate(Temporal.PlainDate.from('2023-06-15')); + const selectedDates = datePicker.getSelectedDates(); + expect(selectedDates).toHaveLength(1); + expect(selectedDates[0]?.toString()).toBe('2023-06-15'); + }); + + test('should select multiple dates correctly', () => { + datePicker.selectDate(Temporal.PlainDate.from('2023-06-15')); + datePicker.selectDate(Temporal.PlainDate.from('2023-06-20')); + const selectedDates = datePicker.getSelectedDates(); + expect(selectedDates).toHaveLength(3); + expect(selectedDates.map(date => date.toString())).toContain('2023-06-15'); + expect(selectedDates.map(date => date.toString())).toContain('2023-06-20'); + }); + + test('should deselect a date correctly in multiple selection mode', () => { + datePicker.selectDate(Temporal.PlainDate.from('2023-06-10')); + const selectedDates = datePicker.getSelectedDates(); + expect(selectedDates).toHaveLength(0); + }); + + test('should select a date range correctly', () => { + datePicker = new DatePicker({ + ...options, + multiple: false, + range: true, + selectedDates: [Temporal.PlainDate.from('2023-06-10')], + }); + datePicker.selectDate(Temporal.PlainDate.from('2023-06-15')); + const selectedDates = datePicker.getSelectedDates(); + expect(selectedDates).toHaveLength(2); + expect(selectedDates.map(date => date.toString())).toContain('2023-06-10'); + expect(selectedDates.map(date => date.toString())).toContain('2023-06-15'); + }); + + test('should not select a date outside the min and max range', () => { + datePicker = new DatePicker({ + ...options, + minDate: Temporal.PlainDate.from('2023-06-05'), + maxDate: Temporal.PlainDate.from('2023-06-20'), + }); + datePicker.selectDate(Temporal.PlainDate.from('2023-06-01')); + datePicker.selectDate(Temporal.PlainDate.from('2023-06-25')); + const selectedDates = datePicker.getSelectedDates(); + expect(selectedDates).toHaveLength(1); + expect(selectedDates[0]?.toString()).toBe('2023-06-10'); + }); +}); From 3f4ef792c3b85122be5248903924d84f081bb295 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:27:22 +0200 Subject: [PATCH 76/95] test: imports --- packages/time/src/tests/calendar-core.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/time/src/tests/calendar-core.test.ts b/packages/time/src/tests/calendar-core.test.ts index 74e6e34..c11605b 100644 --- a/packages/time/src/tests/calendar-core.test.ts +++ b/packages/time/src/tests/calendar-core.test.ts @@ -1,7 +1,7 @@ import { Temporal } from '@js-temporal/polyfill'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { CalendarCore } from '../calendar-core'; -import type { CalendarCoreOptions, Event } from '../calendar-core'; +import { CalendarCore } from '../core/calendar'; +import type { CalendarCoreOptions, Event } from '../core/calendar'; describe('CalendarCore', () => { let options: CalendarCoreOptions; From ce4fa6b2586d3f47a976e54b6273f9587240906b Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:30:48 +0200 Subject: [PATCH 77/95] feat: date picker core --- packages/time/src/tests/calendar-core.test.ts | 4 ++-- .../time/src/tests/date-picker-core.test.ts | 18 ++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/time/src/tests/calendar-core.test.ts b/packages/time/src/tests/calendar-core.test.ts index 74e6e34..c11605b 100644 --- a/packages/time/src/tests/calendar-core.test.ts +++ b/packages/time/src/tests/calendar-core.test.ts @@ -1,7 +1,7 @@ import { Temporal } from '@js-temporal/polyfill'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { CalendarCore } from '../calendar-core'; -import type { CalendarCoreOptions, Event } from '../calendar-core'; +import { CalendarCore } from '../core/calendar'; +import type { CalendarCoreOptions, Event } from '../core/calendar'; describe('CalendarCore', () => { let options: CalendarCoreOptions; diff --git a/packages/time/src/tests/date-picker-core.test.ts b/packages/time/src/tests/date-picker-core.test.ts index 3e2a5f8..372672d 100644 --- a/packages/time/src/tests/date-picker-core.test.ts +++ b/packages/time/src/tests/date-picker-core.test.ts @@ -13,20 +13,6 @@ describe('DatePicker', () => { options = { weekStartsOn: 1, viewMode: { value: 1, unit: 'month' }, - events: [ - { - id: '1', - startDate: Temporal.PlainDateTime.from('2023-06-10T09:00'), - endDate: Temporal.PlainDateTime.from('2023-06-10T10:00'), - title: 'Event 1', - }, - { - id: '2', - startDate: Temporal.PlainDateTime.from('2023-06-12T11:00'), - endDate: Temporal.PlainDateTime.from('2023-06-12T12:00'), - title: 'Event 2', - }, - ], selectedDates: [Temporal.PlainDate.from('2023-06-10')], multiple: true, }; @@ -42,6 +28,10 @@ describe('DatePicker', () => { }); test('should select a date correctly in single selection mode', () => { + datePicker = new DatePicker({ + ...options, + multiple: false, + }); datePicker.selectDate(Temporal.PlainDate.from('2023-06-15')); const selectedDates = datePicker.getSelectedDates(); expect(selectedDates).toHaveLength(1); From bff5f34133f910b35afb4c8505adfc5fa36ed341 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Thu, 20 Jun 2024 23:35:38 +0200 Subject: [PATCH 78/95] docs: typo --- docs/reference/calendar-core.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/calendar-core.md b/docs/reference/calendar-core.md index 8c7b244..22e4b52 100644 --- a/docs/reference/calendar-core.md +++ b/docs/reference/calendar-core.md @@ -55,7 +55,7 @@ The `CalendarCore` class provides a set of functionalities for managing calendar #### Example Usage ```ts -import { CalendarCore, Event } from 'your-calendar-core-package'; +import { CalendarCore, Event } from '@tanstack/time'; import { Temporal } from '@js-temporal/polyfill'; interface MyEvent extends Event { From 1c10f149649ffec08acf4ba61cf62878a8c3e45e Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Fri, 21 Jun 2024 11:15:04 +0200 Subject: [PATCH 79/95] refactor: DatePickerCore --- docs/reference/date-picker-core.md | 109 ++++++++++++++++++++++++++ packages/time/src/core/date-picker.ts | 13 ++- 2 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 docs/reference/date-picker-core.md diff --git a/docs/reference/date-picker-core.md b/docs/reference/date-picker-core.md new file mode 100644 index 0000000..184a234 --- /dev/null +++ b/docs/reference/date-picker-core.md @@ -0,0 +1,109 @@ +--- +title: DatePicker Core +id: date-picker-core +--- + +### `CalendarCore` +```ts +export class DatePickerCore extends CalendarCore { + constructor(options: DatePickerCoreOptions); +} +``` + +The `DatePicker` class extends `CalendarCore` to provide additional functionalities for managing date selection, including single-date, multiple-date, and range selection modes. This class is designed to be used in various date picker applications where precise date management and selection are required. + +#### Parameters + +- `weekStartsOn?: number` + - An optional number that specifies the day of the week that the calendar should start on. It defaults to 1 (Monday). +- `events?: TEvent[]` + - An optional array of events that the calendar should display. +- `viewMode: ViewMode` + - An object that specifies the initial view mode of the calendar. +- `locale?: Parameters['0']` + - An optional string that specifies the locale to use for formatting dates and times. +- `minDate?: Temporal.PlainDate` + - An optional date that specifies the minimum selectable date. +- `maxDate?: Temporal.PlainDate` + - An optional date that specifies the maximum selectable date. +- `selectedDates?: Temporal.PlainDate[]` + - An optional array of dates that are initially selected. +- `multiple?: boolean` + - An optional boolean that allows multiple dates to be selected if set to true. +- `range?: boolean` + - An optional boolean that allows a range of dates to be selected if set to true. + +#### Returns + +- `getSelectedDates(): Temporal.PlainDate[]` + - Returns an array of selected dates. +- `selectDate(date: Temporal.PlainDate): void` + - Selects a date. Depending on the configuration, this can handle single, multiple, or range selections. +- `getDaysWithEvents(): Array>` + - Returns an array of days in the current period with their associated events. +- `getDaysNames(): string[]` + - Returns an array of strings representing the names of the days of the week based on the locale and week start day. +- `changeViewMode(newViewMode: ViewMode): void` + - Changes the view mode of the calendar. +- `goToPreviousPeriod(): void` + - Navigates to the previous period based on the current view mode. +- `goToNextPeriod(): void` + - Navigates to the next period based on the current view mode. +- `goToCurrentPeriod(): void` + - Navigates to the current period. +- `goToSpecificPeriod(date: Temporal.PlainDate): void` + - Navigates to a specific period based on the provided date. +- `updateCurrentTime(): void` + - Updates the current time. +- `getEventProps(id: Event['id']): { style: CSSProperties } | null` + - Retrieves the style properties for a specific event based on its ID. +- `getCurrentTimeMarkerProps(): { style: CSSProperties; currentTime: string | undefined }` + - Retrieves the style properties and current time for the current time marker. +- `groupDaysBy(props: Omit, 'weekStartsOn'>): (Day | null)[][]` + - Groups the days in the current period by a specified unit. The fillMissingDays parameter can be used to fill in missing days with previous or next month's days. + + +#### Example Usage +```ts +import { DatePickerCore, Event } from '@tanstack/time'; +import { Temporal } from '@js-temporal/polyfill'; + +const datePickerCore = new DatePickerCore({ + weekStartsOn: 1, + viewMode: { value: 1, unit: 'month' }, + events, + locale: 'en-US', + minDate: Temporal.PlainDate.from('2024-01-01'), + maxDate: Temporal.PlainDate.from('2024-12-31'), + selectedDates: [Temporal.PlainDate.from('2024-06-10')], + multiple: true, +}); + +// Get selected dates +const selectedDates = datePickerCore.getSelectedDates(); +console.log(selectedDates); + +// Select a date +datePickerCore.selectDate(Temporal.PlainDate.from('2024-06-15')); + +// Change view mode to week +datePickerCore.changeViewMode({ value: 1, unit: 'week' }); + +// Navigate to the next period +datePickerCore.goToNextPeriod(); + +// Update current time +datePickerCore.updateCurrentTime(); + +// Get event properties +const eventProps = datePickerCore.getEventProps('1'); +console.log(eventProps); + +// Get current time marker properties +const currentTimeMarkerProps = datePickerCore.getCurrentTimeMarkerProps(); +console.log(currentTimeMarkerProps); + +// Group days by week +const groupedDays = datePickerCore.groupDaysBy({ days: daysWithSelection, unit: 'week' }); +console.log(groupedDays); +``` diff --git a/packages/time/src/core/date-picker.ts b/packages/time/src/core/date-picker.ts index 005681e..b8e8080 100644 --- a/packages/time/src/core/date-picker.ts +++ b/packages/time/src/core/date-picker.ts @@ -6,24 +6,23 @@ import type { CalendarCoreOptions, CalendarState } from './calendar'; export interface DatePickerOptions extends CalendarCoreOptions { minDate?: Temporal.PlainDate; maxDate?: Temporal.PlainDate; - onSelectDate?: (date: Temporal.PlainDate) => void; multiple?: boolean; range?: boolean; selectedDates?: Temporal.PlainDate[]; } -export interface DatePickerState extends CalendarState { +export interface DatePickerCoreState extends CalendarState { selectedDates: Map; } -export class DatePicker extends CalendarCore { - datePickerStore: Store; +export class DatePickerCore extends CalendarCore { + datePickerStore: Store; options: DatePickerOptions; constructor(options: DatePickerOptions) { super(options); this.options = options; - this.datePickerStore = new Store({ + this.datePickerStore = new Store({ ...this.store.state, selectedDates: new Map(options.selectedDates?.map(date => [date.toString(), date]) ?? []), }); @@ -34,7 +33,7 @@ export class DatePicker extends CalendarCore { } selectDate(date: Temporal.PlainDate) { - const { multiple, range, minDate, maxDate, onSelectDate } = this.options; + const { multiple, range, minDate, maxDate } = this.options; if (minDate && Temporal.PlainDate.compare(date, minDate) < 0) return; if (maxDate && Temporal.PlainDate.compare(date, maxDate) > 0) return; @@ -58,7 +57,5 @@ export class DatePicker extends CalendarCore { ...prev, selectedDates, })); - - onSelectDate?.(date); } } From 57c547efad812ba8384dbc2c4387d9baf008af97 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Fri, 21 Jun 2024 12:42:18 +0200 Subject: [PATCH 80/95] refactor: DatePickerCore --- packages/time/src/core/date-picker.ts | 64 --------------------------- 1 file changed, 64 deletions(-) delete mode 100644 packages/time/src/core/date-picker.ts diff --git a/packages/time/src/core/date-picker.ts b/packages/time/src/core/date-picker.ts deleted file mode 100644 index 30d96b7..0000000 --- a/packages/time/src/core/date-picker.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Store } from '@tanstack/store'; -import { Temporal } from '@js-temporal/polyfill'; -import { CalendarCore } from './calendar'; -import type { CalendarCoreOptions, CalendarState, Event } from './calendar'; - -export interface DatePickerOptions extends CalendarCoreOptions { - minDate?: Temporal.PlainDate; - maxDate?: Temporal.PlainDate; - onSelectDate?: (date: Temporal.PlainDate) => void; - multiple?: boolean; - range?: boolean; - selectedDates?: Temporal.PlainDate[]; -} - -export interface DatePickerState extends CalendarState { - selectedDates: Map; -} - -export class DatePicker extends CalendarCore { - datePickerStore: Store; - options: DatePickerOptions; - - constructor(options: DatePickerOptions) { - super(options); - this.options = options; - this.datePickerStore = new Store({ - ...this.store.state, - selectedDates: new Map(options.selectedDates?.map(date => [date.toString(), date]) ?? []), - }); - } - - getSelectedDates() { - return Array.from(this.datePickerStore.state.selectedDates.values()); - } - - selectDate(date: Temporal.PlainDate) { - const { multiple, range, minDate, maxDate, onSelectDate } = this.options; - - if (minDate && Temporal.PlainDate.compare(date, minDate) < 0) return; - if (maxDate && Temporal.PlainDate.compare(date, maxDate) > 0) return; - - const selectedDates = new Map(this.datePickerStore.state.selectedDates); - - if (range && selectedDates.size === 1) { - selectedDates.set(date.toString(), date); - } else if (multiple) { - if (selectedDates.has(date.toString())) { - selectedDates.delete(date.toString()); - } else { - selectedDates.set(date.toString(), date); - } - } else { - selectedDates.clear(); - selectedDates.set(date.toString(), date); - } - - this.datePickerStore.setState(prev => ({ - ...prev, - selectedDates, - })); - - onSelectDate?.(date); - } -} From 3dc223d4f564efe2053b59c6f69d09d16766eb7b Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 23 Jun 2024 23:26:06 +0200 Subject: [PATCH 81/95] refactor: weekInfoPolyfill --- packages/time/src/core/calendar.ts | 2 + packages/time/src/core/weekInfoPolyfill.ts | 3213 ++++++++++++++++++++ 2 files changed, 3215 insertions(+) create mode 100644 packages/time/src/core/weekInfoPolyfill.ts diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 4412972..daa2aed 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -9,6 +9,8 @@ import type { Properties as CSSProperties } from 'csstype'; import type { GroupDaysByProps } from '../calendar/groupDaysBy'; import type { CalendarState, Day, Event } from '../calendar/types'; +import './weekInfoPolyfill' + export type { CalendarState, Event, Day } from '../calendar/types'; export interface ViewMode { diff --git a/packages/time/src/core/weekInfoPolyfill.ts b/packages/time/src/core/weekInfoPolyfill.ts new file mode 100644 index 0000000..e3e73f3 --- /dev/null +++ b/packages/time/src/core/weekInfoPolyfill.ts @@ -0,0 +1,3213 @@ +interface WeekInfo { + firstDay: number + weekend: number[] + minimalDays: number +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Intl { + interface Locale { + getWeekInfo: () => WeekInfo + } +} + +;(function () { + if (typeof (Intl as any).Locale.prototype.getWeekInfo !== 'function') { + ;(Intl as any).Locale.prototype.getWeekInfo = function () { + const locale = this.toString().toLowerCase() + const weekInfo: Record = { + af_NA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + af_ZA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + af: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ak_GH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ak: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sq_AL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sq: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + am_ET: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + am: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_DZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_BH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_EG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_IQ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_JO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_KW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_LB: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_LY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_MA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_OM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_QA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_SA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_SD: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_SY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_TN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_AE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar_YE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ar: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hy_AM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hy: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + as_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + as: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + asa_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + asa: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + az_Cyrl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + az_Latn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + az: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bm_ML: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bm: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + eu_ES: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + eu: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + be_BY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + be: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bem_ZM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bem: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bez_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bez: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bn_BD: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bn_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bs_BA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bs: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bg_BG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bg: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + my_MM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + my: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ca_ES: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ca: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + tzm_Latn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + tzm: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + chr_US: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + chr: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + cgg_UG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + cgg: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + zh_Hans: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + zh_Hant: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + zh: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kw_GB: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kw: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hr_HR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hr: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + cs_CZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + cs: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + da_DK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + da: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nl_BE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nl_NL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ebu_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ebu: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_AS: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_AU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_BE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_BZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_BW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_CA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_GU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_HK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_IE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_IL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_JM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_MT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_MH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_MU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_NA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_NZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_MP: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_PK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_PH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_SG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_ZA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_TT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_UM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_VI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_GB: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_US: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en_ZW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + en: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + eo: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + et_EE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + et: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ee_GH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ee_TG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ee: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fo_FO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fo: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fil_PH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fil: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fi_FI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fi: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_BE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_BJ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_BF: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_BI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CF: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_TD: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_KM: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CD: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_DJ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_GQ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_FR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_GA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_GP: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_GN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_LU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_MG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_ML: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_MQ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_MC: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_NE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_RW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_RE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_BL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_MF: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_SN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_CH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr_TG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fr: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ff_SN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ff: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gl_ES: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lg_UG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lg: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ka_GE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ka: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de_AT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de_BE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de_DE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de_LI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de_LU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de_CH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + de: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + el_CY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + el_GR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + el: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gu_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gu: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + guz_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + guz: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ha_Latn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ha: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + haw_US: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + haw: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + he_IL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + he: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hi_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hi: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hu_HU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + hu: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + is_IS: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + is: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ig_NG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ig: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + id_ID: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + id: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ga_IE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ga: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + it_IT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + it_CH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + it: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ja_JP: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ja: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kea_CV: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kea: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kab_DZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kab: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kl_GL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kln_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kln: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kam_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kam: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kn_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kk_Cyrl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kk: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + km_KH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + km: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ki_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ki: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rw_RW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rw: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kok_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kok: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ko_KR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ko: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + khq_ML: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + khq: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ses_ML: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ses: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lag_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lag: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lv_LV: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lv: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lt_LT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + lt: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + luo_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + luo: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + luy_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + luy: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mk_MK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mk: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + jmc_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + jmc: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kde_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + kde: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mg_MG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mg: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ms_BN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ms_MY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ms: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ml_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ml: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mt_MT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mt: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gv_GB: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gv: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mr_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mr: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mas_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mas_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mas: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mer_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mer: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mfe_MU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + mfe: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + naq_NA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + naq: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ne_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ne_NP: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ne: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nd_ZW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nd: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nb_NO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nb: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nn_NO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nyn_UG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + nyn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + or_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + or: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + om_ET: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + om_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + om: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ps_AF: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ps: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fa_AF: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fa_IR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + fa: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pl_PL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pt_BR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pt_GW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pt_MZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pt_PT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pt: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pa_Arab: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pa_Guru: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + pa: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ro_MD: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ro_RO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ro: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rm_CH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rm: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rof_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rof: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ru_MD: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ru_RU: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ru_UA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ru: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rwk_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + rwk: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + saq_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + saq: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sg_CF: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sg: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + seh_MZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + seh: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sr_Cyrl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sr_Latn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sr: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sn_ZW: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ii_CN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ii: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + si_LK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + si: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sk_SK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sk: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sl_SI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + xog_UG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + xog: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + so_DJ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + so_ET: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + so_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + so_SO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + so: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_AR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_BO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_CL: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_CO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_CR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_DO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_EC: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_SV: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_GQ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_GT: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_HN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_419: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_MX: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_NI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_PA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_PY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_PE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_PR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_ES: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_US: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_UY: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es_VE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + es: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sw_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sw_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sw: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sv_FI: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sv_SE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + sv: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gsw_CH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + gsw: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + shi_Latn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + shi_Tfng: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + shi: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + dav_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + dav: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ta_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ta_LK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ta: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + te_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + te: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + teo_KE: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + teo_UG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + teo: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + th_TH: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + th: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bo_CN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bo_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + bo: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ti_ER: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ti_ET: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ti: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + to_TO: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + to: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + tr_TR: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + tr: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + uk_UA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + uk: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ur_IN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ur_PK: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + ur: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + uz_Arab: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + uz_Cyrl: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + uz_Latn: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + uz: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + vi_VN: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + vi: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + vun_TZ: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + vun: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + cy_GB: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + cy: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + yo_NG: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + yo: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + zu_ZA: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + zu: { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'af-ZA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'am-ET': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-AE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-BH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-DZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-EG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-IQ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-JO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-KW': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-LB': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-LY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-MA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'arn-CL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-OM': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-QA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-SA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-SD': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-SY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-TN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ar-YE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'as-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'az-az': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'az-Cyrl-AZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'az-Latn-AZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ba-RU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'be-BY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'bg-BG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'bn-BD': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'bn-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'bo-CN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'br-FR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'bs-Cyrl-BA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'bs-Latn-BA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ca-ES': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'co-FR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'cs-CZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'cy-GB': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'da-DK': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'de-AT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'de-CH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'de-DE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'de-LI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'de-LU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'dsb-DE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'dv-MV': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'el-CY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'el-GR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-029': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-AU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-BZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-CA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-cb': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-GB': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-IE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-JM': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-MT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-MY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-NZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-PH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-SG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-TT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-US': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-ZA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'en-ZW': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-AR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-BO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-CL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-CO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-CR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-DO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-EC': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-ES': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-GT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-HN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-MX': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-NI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-PA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-PE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-PR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-PY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-SV': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-US': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-UY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'es-VE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'et-EE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'eu-ES': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fa-IR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fi-FI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fil-PH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fo-FO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fr-BE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fr-CA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fr-CH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fr-FR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fr-LU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fr-MC': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'fy-NL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ga-IE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'gd-GB': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'gd-ie': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'gl-ES': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'gsw-FR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'gu-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ha-Latn-NG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'he-IL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'hi-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'hr-BA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'hr-HR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'hsb-DE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'hu-HU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'hy-AM': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'id-ID': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ig-NG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ii-CN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'in-ID': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'is-IS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'it-CH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'it-IT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'iu-Cans-CA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'iu-Latn-CA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'iw-IL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ja-JP': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ka-GE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'kk-KZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'kl-GL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'km-KH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'kn-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'kok-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ko-KR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ky-KG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'lb-LU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'lo-LA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'lt-LT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'lv-LV': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'mi-NZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'mk-MK': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ml-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'mn-MN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'mn-Mong-CN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'moh-CA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'mr-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ms-BN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ms-MY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'mt-MT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'nb-NO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ne-NP': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'nl-BE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'nl-NL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'nn-NO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'no-no': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'nso-ZA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'oc-FR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'or-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'pa-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'pl-PL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'prs-AF': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ps-AF': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'pt-BR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'pt-PT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'qut-GT': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'quz-BO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'quz-EC': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'quz-PE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'rm-CH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ro-mo': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ro-RO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ru-mo': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ru-RU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'rw-RW': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sah-RU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sa-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'se-FI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'se-NO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'se-SE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'si-LK': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sk-SK': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sl-SI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sma-NO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sma-SE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'smj-NO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'smj-SE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'smn-FI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sms-FI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sq-AL': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-BA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-CS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Cyrl-BA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Cyrl-CS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Cyrl-ME': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Cyrl-RS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Latn-BA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Latn-CS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Latn-ME': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-Latn-RS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-ME': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-RS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sr-sp': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sv-FI': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sv-SE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'sw-KE': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'syr-SY': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ta-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'te-IN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tg-Cyrl-TJ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'th-TH': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tk-TM': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tlh-QS': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tn-ZA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tr-TR': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tt-RU': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'tzm-Latn-DZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ug-CN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'uk-UA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'ur-PK': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'uz-Cyrl-UZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'uz-Latn-UZ': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'uz-uz': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'vi-VN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'wo-SN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'xh-ZA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'yo-NG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'zh-CN': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'zh-HK': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'zh-MO': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'zh-SG': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'zh-TW': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + 'zu-ZA': { + firstDay: 1, + weekend: [6, 7], + minimalDays: 4, + }, + } + + const match = + weekInfo[locale] || + weekInfo[locale.split('-')[0]] || + weekInfo['default'] + + return { + firstDay: match?.firstDay, + weekend: match?.weekend, + minimalDays: match?.minimalDays, + } + } + } +})() From 2eabee40d5f93441a9a866befa7a1e677bd5a278 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Sun, 23 Jun 2024 23:43:29 +0200 Subject: [PATCH 82/95] refactor: weekInfoPolyfill --- packages/time/src/core/calendar.ts | 8 ++++---- packages/time/src/utils/getFirstDayOfWeek.ts | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index daa2aed..834a2e8 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -9,7 +9,7 @@ import type { Properties as CSSProperties } from 'csstype'; import type { GroupDaysByProps } from '../calendar/groupDaysBy'; import type { CalendarState, Day, Event } from '../calendar/types'; -import './weekInfoPolyfill' +import './weekInfoPolyfill'; export type { CalendarState, Event, Day } from '../calendar/types'; @@ -22,7 +22,7 @@ export interface CalendarCoreOptions { weekStartsOn?: number; events?: TEvent[]; viewMode: CalendarState['viewMode']; - locale?: Parameters['0']; + locale?: string; } export interface CalendarApi { @@ -49,7 +49,7 @@ export class CalendarCore { options: CalendarCoreOptions; constructor(options: CalendarCoreOptions) { - this.options = options; + this.options = options; this.store = new Store({ currentPeriod: Temporal.Now.plainDateISO(), viewMode: options.viewMode, @@ -64,7 +64,7 @@ export class CalendarCore { } private getFirstDayOfWeek() { - return getFirstDayOfWeek(this.store.state.currentPeriod.toString(), this.options.weekStartsOn ?? 1); + return getFirstDayOfWeek(this.store.state.currentPeriod.toString(), this.options.locale); } private getCalendarDays() { diff --git a/packages/time/src/utils/getFirstDayOfWeek.ts b/packages/time/src/utils/getFirstDayOfWeek.ts index bb02180..701b1ed 100644 --- a/packages/time/src/utils/getFirstDayOfWeek.ts +++ b/packages/time/src/utils/getFirstDayOfWeek.ts @@ -1,6 +1,8 @@ import { Temporal } from '@js-temporal/polyfill' -export const getFirstDayOfWeek = (currWeek: string, weekStartsOn: number) => { - const date = Temporal.PlainDate.from(currWeek) - return date.subtract({ days: (date.dayOfWeek - weekStartsOn + 7) % 7 }) +export const getFirstDayOfWeek = (currWeek: string, locale: string = 'en-US') => { + const date = Temporal.PlainDate.from(currWeek); + const loc = new Intl.Locale(locale); + const { firstDay } = loc.getWeekInfo(); + return date.subtract({ days: (date.dayOfWeek - firstDay + 7) % 7 }); } From 564c4e7d58be50b726dce17483e9364c5fcd1b04 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 24 Jun 2024 00:31:41 +0200 Subject: [PATCH 83/95] refactor: weekInfoPolyfill --- packages/time/src/core/weekInfoPolyfill.ts | 1590 ++------------------ 1 file changed, 120 insertions(+), 1470 deletions(-) diff --git a/packages/time/src/core/weekInfoPolyfill.ts b/packages/time/src/core/weekInfoPolyfill.ts index e3e73f3..70f0558 100644 --- a/packages/time/src/core/weekInfoPolyfill.ts +++ b/packages/time/src/core/weekInfoPolyfill.ts @@ -16,1832 +16,602 @@ declare namespace Intl { ;(Intl as any).Locale.prototype.getWeekInfo = function () { const locale = this.toString().toLowerCase() const weekInfo: Record = { - af_NA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - af_ZA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - af: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ak_GH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ak: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - sq_AL: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - sq: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - am_ET: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - am: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_DZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_BH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_EG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_IQ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_JO: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_KW: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_LB: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_LY: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_MA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_OM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_QA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_SA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_SD: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_SY: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_TN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_AE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar_YE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ar: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hy_AM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hy: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - as_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - as: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - asa_TZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - asa: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - az_Cyrl: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - az_Latn: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - az: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bm_ML: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bm: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - eu_ES: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - eu: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - be_BY: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - be: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bem_ZM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bem: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bez_TZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bez: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bn_BD: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bn_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bn: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bs_BA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bs: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bg_BG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bg: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - my_MM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - my: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ca_ES: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ca: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - tzm_Latn: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - tzm: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - chr_US: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - chr: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - cgg_UG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - cgg: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - zh_Hans: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - zh_Hant: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - zh: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kw_GB: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kw: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hr_HR: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hr: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - cs_CZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - cs: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - da_DK: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - da: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - nl_BE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - nl_NL: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - nl: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ebu_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ebu: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_AS: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_AU: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_BE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_BZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_BW: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_CA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_GU: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_HK: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_IE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_IL: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_JM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_MT: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_MH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_MU: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_NA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_NZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_MP: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_PK: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_PH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_SG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_ZA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_TT: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_UM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_VI: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_GB: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_US: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en_ZW: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - en: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - eo: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - et_EE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - et: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ee_GH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ee_TG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ee: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fo_FO: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fo: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fil_PH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fil: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fi_FI: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fi: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_BE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_BJ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_BF: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_BI: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CF: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_TD: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_KM: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CD: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CI: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_DJ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_GQ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_FR: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_GA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_GP: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_GN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_LU: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_MG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_ML: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_MQ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_MC: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_NE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_RW: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_RE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_BL: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_MF: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_SN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_CH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr_TG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - fr: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ff_SN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ff: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - gl_ES: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - gl: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lg_UG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lg: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ka_GE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ka: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de_AT: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de_BE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de_DE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de_LI: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de_LU: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de_CH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - de: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - el_CY: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - el_GR: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - el: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - gu_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - gu: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - guz_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - guz: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ha_Latn: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ha: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - haw_US: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - haw: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - he_IL: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - he: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hi_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hi: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hu_HU: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - hu: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - is_IS: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - is: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ig_NG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ig: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - id_ID: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - id: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ga_IE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ga: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - it_IT: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - it_CH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - it: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ja_JP: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ja: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kea_CV: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kea: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kab_DZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kab: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kl_GL: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kl: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kln_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kln: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kam_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kam: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kn_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kn: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kk_Cyrl: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kk: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - km_KH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - km: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ki_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ki: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - rw_RW: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - rw: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kok_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kok: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ko_KR: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ko: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - khq_ML: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - khq: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ses_ML: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ses: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lag_TZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lag: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lv_LV: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lv: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lt_LT: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - lt: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - luo_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - luo: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - luy_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - luy: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - mk_MK: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - mk: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - jmc_TZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - jmc: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kde_TZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - kde: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - mg_MG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - mg: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ms_BN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ms_MY: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ms: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ml_IN: { + af: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ml: { + ak: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mt_MT: { + sq: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mt: { + am: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - gv_GB: { + ar: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - gv: { + hy: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mr_IN: { + as: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mr: { + asa: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mas_KE: { + az: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mas_TZ: { + bm: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mas: { + eu: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mer_KE: { + be: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mer: { + bem: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mfe_MU: { + bez: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - mfe: { + bn: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - naq_NA: { + bs: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - naq: { + bg: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ne_IN: { + my: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ne_NP: { + ca: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ne: { + tzm: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nd_ZW: { + chr: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nd: { + cgg: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nb_NO: { + zh: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nb: { + kw: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nn_NO: { + hr: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nn: { + cs: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nyn_UG: { + da: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - nyn: { + nl: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - or_IN: { + ebu: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - or: { + en: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - om_ET: { + eo: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - om_KE: { + et: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - om: { + ee: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ps_AF: { + fo: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ps: { + fil: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - fa_AF: { + fi: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - fa_IR: { + fr: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - fa: { + ff: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pl_PL: { + gl: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pl: { + lg: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pt_BR: { + ka: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pt_GW: { + de: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pt_MZ: { + el: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pt_PT: { + gu: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pt: { + guz: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pa_Arab: { + ha: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pa_Guru: { + haw: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - pa: { + he: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ro_MD: { + hi: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ro_RO: { + hu: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ro: { + is: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - rm_CH: { + ig: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - rm: { + id: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - rof_TZ: { + ga: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - rof: { + it: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ru_MD: { + ja: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ru_RU: { + kea: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ru_UA: { + kab: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ru: { + kl: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - rwk_TZ: { + kln: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - rwk: { + kam: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - saq_KE: { + kn: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - saq: { + kk: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sg_CF: { + km: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sg: { + ki: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - seh_MZ: { + rw: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - seh: { + kok: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sr_Cyrl: { + ko: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sr_Latn: { + khq: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sr: { + ses: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sn_ZW: { + lag: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sn: { + lv: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ii_CN: { + lt: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ii: { + luo: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - si_LK: { + luy: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - si: { + mk: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sk_SK: { + jmc: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sk: { + kde: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sl_SI: { + mg: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sl: { + ms: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - xog_UG: { + ml: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - xog: { + mt: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - so_DJ: { + gv: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - so_ET: { + mr: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - so_KE: { + mas: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - so_SO: { + mer: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - so: { + mfe: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_AR: { + naq: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_BO: { + ne: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_CL: { + nd: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_CO: { + nb: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_CR: { + nn: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_DO: { + nyn: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_EC: { + or: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_SV: { + om: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_GQ: { + ps: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_GT: { + fa: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_HN: { + pl: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_419: { + pt: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_MX: { + pa: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_NI: { + ro: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_PA: { + rm: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_PY: { + rof: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_PE: { + ru: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_PR: { + rwk: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_ES: { + saq: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_US: { + sg: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_UY: { + seh: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es_VE: { + sr: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - es: { + sn: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sw_KE: { + ii: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sw_TZ: { + si: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sw: { + sk: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sv_FI: { + sl: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sv_SE: { + xog: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - sv: { + so: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - gsw_CH: { + es: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - gsw: { + sw: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - shi_Latn: { + sv: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - shi_Tfng: { + gsw: { firstDay: 1, weekend: [6, 7], minimalDays: 4, @@ -1851,206 +621,86 @@ declare namespace Intl { weekend: [6, 7], minimalDays: 4, }, - dav_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, dav: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ta_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ta_LK: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, ta: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - te_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, te: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - teo_KE: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - teo_UG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, teo: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - th_TH: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, th: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - bo_CN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - bo_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, bo: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ti_ER: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ti_ET: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, ti: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - to_TO: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, to: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - tr_TR: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, tr: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - uk_UA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, uk: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - ur_IN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - ur_PK: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, ur: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - uz_Arab: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - uz_Cyrl: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, - uz_Latn: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, uz: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - vi_VN: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, vi: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - vun_TZ: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, vun: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - cy_GB: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, cy: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - yo_NG: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, yo: { firstDay: 1, weekend: [6, 7], minimalDays: 4, }, - zu_ZA: { - firstDay: 1, - weekend: [6, 7], - minimalDays: 4, - }, zu: { firstDay: 1, weekend: [6, 7], From 1a5f1937ebd80184764ab1332c7b1eb46d46d4d6 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 24 Jun 2024 10:13:56 +0200 Subject: [PATCH 84/95] refactor: weekInfoPolyfill --- packages/time/src/core/weekInfoPolyfill.ts | 1 + packages/time/src/utils/getFirstDayOfWeek.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/time/src/core/weekInfoPolyfill.ts b/packages/time/src/core/weekInfoPolyfill.ts index 70f0558..28c7351 100644 --- a/packages/time/src/core/weekInfoPolyfill.ts +++ b/packages/time/src/core/weekInfoPolyfill.ts @@ -8,6 +8,7 @@ interface WeekInfo { declare namespace Intl { interface Locale { getWeekInfo: () => WeekInfo + weekInfo?: WeekInfo } } diff --git a/packages/time/src/utils/getFirstDayOfWeek.ts b/packages/time/src/utils/getFirstDayOfWeek.ts index 701b1ed..1b33e98 100644 --- a/packages/time/src/utils/getFirstDayOfWeek.ts +++ b/packages/time/src/utils/getFirstDayOfWeek.ts @@ -3,6 +3,6 @@ import { Temporal } from '@js-temporal/polyfill' export const getFirstDayOfWeek = (currWeek: string, locale: string = 'en-US') => { const date = Temporal.PlainDate.from(currWeek); const loc = new Intl.Locale(locale); - const { firstDay } = loc.getWeekInfo(); + const { firstDay } = loc.weekInfo || loc.getWeekInfo(); return date.subtract({ days: (date.dayOfWeek - firstDay + 7) % 7 }); } From 3331c174e061d9b45fb193fb46b3a11e5951a0bf Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 24 Jun 2024 21:46:29 +0200 Subject: [PATCH 85/95] refactor: date defaults --- packages/time/src/core/calendar.ts | 11 ++++++----- packages/time/src/tests/calendar-core.test.ts | 2 -- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 834a2e8..5e5fa9e 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -19,10 +19,11 @@ export interface ViewMode { } export interface CalendarCoreOptions { - weekStartsOn?: number; events?: TEvent[]; viewMode: CalendarState['viewMode']; locale?: string; + timeZone?: string; + calendar?: string; } export interface CalendarApi { @@ -71,7 +72,7 @@ export class CalendarCore { const start = this.store.state.viewMode.unit === 'month' ? this.getFirstDayOfMonth().subtract({ - days: (this.getFirstDayOfMonth().dayOfWeek - (this.options.weekStartsOn ?? 1) + 7) % 7, + days: (this.getFirstDayOfMonth().dayOfWeek - (this.getFirstDayOfWeek().dayOfWeek + 1) + 7) % 7, }) : this.store.state.currentPeriod; @@ -82,7 +83,7 @@ export class CalendarCore { .add({ months: this.store.state.viewMode.value }) .subtract({ days: 1 }); const lastDayOfMonthWeekDay = - (lastDayOfMonth.dayOfWeek - (this.options.weekStartsOn ?? 1) + 7) % 7; + (lastDayOfMonth.dayOfWeek - (this.getFirstDayOfWeek().dayOfWeek + 1) + 7) % 7; end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }); break; } @@ -162,7 +163,7 @@ export class CalendarCore { const baseDate = Temporal.PlainDate.from('2024-01-01'); return Array.from({ length: 7 }).map((_, i) => baseDate - .add({ days: (i + (this.options.weekStartsOn ?? 1) - 1) % 7 }) + .add({ days: (i + (this.getFirstDayOfWeek().dayOfWeek + 1)) % 7 }) .toLocaleString(this.options.locale, { weekday: 'short' }), ); } @@ -283,6 +284,6 @@ export class CalendarCore { } groupDaysBy({ days, unit, fillMissingDays = true }: Omit, 'weekStartsOn'>) { - return groupDaysBy({ days, unit, fillMissingDays, weekStartsOn: this.options.weekStartsOn ?? 1 } as GroupDaysByProps); + return groupDaysBy({ days, unit, fillMissingDays, weekStartsOn: this.getFirstDayOfWeek().dayOfWeek } as GroupDaysByProps); } } diff --git a/packages/time/src/tests/calendar-core.test.ts b/packages/time/src/tests/calendar-core.test.ts index c11605b..29f4234 100644 --- a/packages/time/src/tests/calendar-core.test.ts +++ b/packages/time/src/tests/calendar-core.test.ts @@ -11,7 +11,6 @@ describe('CalendarCore', () => { beforeEach(() => { options = { - weekStartsOn: 1, viewMode: { value: 1, unit: 'month' }, events: [ { @@ -100,7 +99,6 @@ describe('CalendarCore', () => { vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(Temporal.PlainDateTime.from('2024-06-01T11:00:00')); const coreOptions: CalendarCoreOptions = { - weekStartsOn: 1, viewMode: { value: 1, unit: 'week' }, events: [], }; From e1f9c12aea6fb5225c8ad1e63ec2c929fb86fd50 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 24 Jun 2024 22:53:48 +0200 Subject: [PATCH 86/95] refactor: date defaults --- .../time/src/calendar/splitMultiDayEvents.ts | 8 ++--- packages/time/src/core/calendar.ts | 31 ++++++++++++------- packages/time/src/tests/calendar-core.test.ts | 29 +++++++++++++---- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/packages/time/src/calendar/splitMultiDayEvents.ts b/packages/time/src/calendar/splitMultiDayEvents.ts index ebb0c2a..e5bdddd 100644 --- a/packages/time/src/calendar/splitMultiDayEvents.ts +++ b/packages/time/src/calendar/splitMultiDayEvents.ts @@ -1,13 +1,13 @@ import { Temporal } from '@js-temporal/polyfill'; import type { Event } from './types'; -export const splitMultiDayEvents = (event: TEvent): TEvent[] => { - const startDate = Temporal.PlainDateTime.from(event.startDate); - const endDate = Temporal.PlainDateTime.from(event.endDate); +export const splitMultiDayEvents = (event: TEvent, timeZone: Temporal.TimeZoneLike): TEvent[] => { + const startDate = event.startDate.toZonedDateTime(timeZone); + const endDate = event.endDate.toZonedDateTime(timeZone); const events: TEvent[] = []; let currentDay = startDate; - while (Temporal.PlainDate.compare(currentDay.toPlainDate(), endDate.toPlainDate()) < 0) { + while (Temporal.ZonedDateTime.compare(currentDay, endDate) < 0) { const startOfCurrentDay = currentDay.with({ hour: 0, minute: 0, second: 0, millisecond: 0 }); const endOfCurrentDay = currentDay.with({ hour: 23, minute: 59, second: 59, millisecond: 999 }); diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 5e5fa9e..f70a736 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -5,6 +5,7 @@ import { generateDateRange } from '../calendar/generateDateRange'; import { splitMultiDayEvents } from '../calendar/splitMultiDayEvents'; import { getEventProps } from '../calendar/getEventProps'; import { groupDaysBy } from '../calendar/groupDaysBy'; +import { getDateDefaults } from '../utils/dateDefaults'; import type { Properties as CSSProperties } from 'csstype'; import type { GroupDaysByProps } from '../calendar/groupDaysBy'; import type { CalendarState, Day, Event } from '../calendar/types'; @@ -19,7 +20,7 @@ export interface ViewMode { } export interface CalendarCoreOptions { - events?: TEvent[]; + events?: TEvent[] | null; viewMode: CalendarState['viewMode']; locale?: string; timeZone?: string; @@ -47,14 +48,22 @@ export interface CalendarApi { export class CalendarCore { store: Store; - options: CalendarCoreOptions; + options: Required>; constructor(options: CalendarCoreOptions) { - this.options = options; + const defaults = getDateDefaults(); + this.options = { + ...options, + locale: options.locale || defaults.locale, + timeZone: options.timeZone || defaults.timeZone, + calendar: options.calendar || defaults.calendar, + events: options.events || null, + }; + this.store = new Store({ - currentPeriod: Temporal.Now.plainDateISO(), + currentPeriod: Temporal.Now.plainDateISO().withCalendar(this.options.calendar), viewMode: options.viewMode, - currentTime: Temporal.Now.plainDateTimeISO(), + currentTime: Temporal.Now.plainDateTimeISO(this.options.timeZone), }); } @@ -111,15 +120,15 @@ export class CalendarCore { private getEventMap() { const map = new Map(); this.options.events?.forEach((event) => { - const eventStartDate = Temporal.PlainDateTime.from(event.startDate); - const eventEndDate = Temporal.PlainDateTime.from(event.endDate); + const eventStartDate = event.startDate.toZonedDateTime(this.options.timeZone); + const eventEndDate = event.endDate.toZonedDateTime(this.options.timeZone); if ( - Temporal.PlainDate.compare( - eventStartDate.toPlainDate(), - eventEndDate.toPlainDate(), + Temporal.ZonedDateTime.compare( + eventStartDate, + eventEndDate, ) !== 0 ) { - const splitEvents = splitMultiDayEvents(event); + const splitEvents = splitMultiDayEvents(event, this.options.timeZone); splitEvents.forEach((splitEvent) => { const splitKey = splitEvent.startDate.toString().split('T')[0]; if (splitKey) { diff --git a/packages/time/src/tests/calendar-core.test.ts b/packages/time/src/tests/calendar-core.test.ts index 29f4234..22f6697 100644 --- a/packages/time/src/tests/calendar-core.test.ts +++ b/packages/time/src/tests/calendar-core.test.ts @@ -8,6 +8,7 @@ describe('CalendarCore', () => { let calendarCore: CalendarCore; const mockDate = Temporal.PlainDate.from('2023-06-15'); const mockDateTime = Temporal.PlainDateTime.from('2023-06-15T10:00'); + const mockTimeZone = 'America/New_York'; beforeEach(() => { options = { @@ -26,6 +27,7 @@ describe('CalendarCore', () => { title: 'Event 2', }, ], + timeZone: mockTimeZone, }; calendarCore = new CalendarCore(options); vi.spyOn(Temporal.Now, 'plainDateISO').mockReturnValue(mockDate); @@ -89,7 +91,7 @@ describe('CalendarCore', () => { const initialTime = calendarCore.store.state.currentTime; const newMockDateTime = initialTime.add({ minutes: 1 }); vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(newMockDateTime); - + calendarCore.updateCurrentTime(); const updatedTime = calendarCore.store.state.currentTime; expect(updatedTime).toEqual(newMockDateTime); @@ -101,6 +103,7 @@ describe('CalendarCore', () => { const coreOptions: CalendarCoreOptions = { viewMode: { value: 1, unit: 'week' }, events: [], + timeZone: mockTimeZone, }; calendarCore = new CalendarCore(coreOptions); @@ -116,18 +119,32 @@ describe('CalendarCore', () => { }); }); - test('should update the current time on', () => { + test('should update the current time', () => { const initialTime = calendarCore.store.state.currentTime; const newMockDateTime = initialTime.add({ minutes: 1 }); vi.spyOn(Temporal.Now, 'plainDateTimeISO').mockReturnValue(newMockDateTime); - + calendarCore.updateCurrentTime(); - expect(initialTime).toEqual(newMockDateTime); - }) + expect(calendarCore.store.state.currentTime).toEqual(newMockDateTime); + }); test('should group days correctly', () => { const daysWithEvents = calendarCore.getDaysWithEvents(); - const groupedDays = calendarCore.groupDaysBy({ days: daysWithEvents, unit: 'month'}); + const groupedDays = calendarCore.groupDaysBy({ days: daysWithEvents, unit: 'month' }); expect(groupedDays.length).toBeGreaterThan(0); }); + + test('should initialize with the correct time zone', () => { + expect(calendarCore.options.timeZone).toBe(mockTimeZone); + }); + + test('should respect custom calendar', () => { + const customCalendar = 'islamic-civil'; + options.calendar = customCalendar; + calendarCore = new CalendarCore(options); + + const today = Temporal.Now.plainDateISO(customCalendar); + expect(calendarCore.store.state.currentPeriod.calendarId).toBe(customCalendar); + expect(calendarCore.store.state.currentPeriod).toEqual(today); + }); }); From 1dfecfbdba39238040c85fed2b3d925f15640253 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Mon, 24 Jun 2024 23:02:26 +0200 Subject: [PATCH 87/95] refactor: date defaults --- packages/time/src/calendar/splitMultiDayEvents.ts | 4 ++-- packages/time/src/calendar/types.ts | 4 ++-- packages/time/src/core/calendar.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/time/src/calendar/splitMultiDayEvents.ts b/packages/time/src/calendar/splitMultiDayEvents.ts index e5bdddd..0d2088d 100644 --- a/packages/time/src/calendar/splitMultiDayEvents.ts +++ b/packages/time/src/calendar/splitMultiDayEvents.ts @@ -2,8 +2,8 @@ import { Temporal } from '@js-temporal/polyfill'; import type { Event } from './types'; export const splitMultiDayEvents = (event: TEvent, timeZone: Temporal.TimeZoneLike): TEvent[] => { - const startDate = event.startDate.toZonedDateTime(timeZone); - const endDate = event.endDate.toZonedDateTime(timeZone); + const startDate = event.startDate instanceof Temporal.PlainDateTime ? event.startDate.toZonedDateTime(timeZone) : event.startDate; + const endDate = event.endDate instanceof Temporal.PlainDateTime ? event.endDate.toZonedDateTime(timeZone) : event.endDate; const events: TEvent[] = []; let currentDay = startDate; diff --git a/packages/time/src/calendar/types.ts b/packages/time/src/calendar/types.ts index 8da9ad3..c3376b3 100644 --- a/packages/time/src/calendar/types.ts +++ b/packages/time/src/calendar/types.ts @@ -2,8 +2,8 @@ import type { Temporal } from "@js-temporal/polyfill" export interface Event { id: string; - startDate: Temporal.PlainDateTime; - endDate: Temporal.PlainDateTime; + startDate: Temporal.PlainDateTime | Temporal.ZonedDateTime; + endDate: Temporal.PlainDateTime | Temporal.ZonedDateTime; title: string; } diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index f70a736..c220b44 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -120,8 +120,8 @@ export class CalendarCore { private getEventMap() { const map = new Map(); this.options.events?.forEach((event) => { - const eventStartDate = event.startDate.toZonedDateTime(this.options.timeZone); - const eventEndDate = event.endDate.toZonedDateTime(this.options.timeZone); + const eventStartDate = event.startDate instanceof Temporal.PlainDateTime ? event.startDate.toZonedDateTime(this.options.timeZone) : event.startDate; + const eventEndDate = event.endDate instanceof Temporal.PlainDateTime ? event.endDate.toZonedDateTime(this.options.timeZone) : event.endDate; if ( Temporal.ZonedDateTime.compare( eventStartDate, From 277f416cc4e363e0170ead2f8bee347395e3718b Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 11:25:45 +0200 Subject: [PATCH 88/95] refactor: types --- packages/time/src/core/calendar.ts | 6 +++--- packages/time/src/utils/dateDefaults.ts | 8 +++++--- packages/time/src/utils/getFirstDayOfWeek.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index c220b44..2a697b5 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -22,9 +22,9 @@ export interface ViewMode { export interface CalendarCoreOptions { events?: TEvent[] | null; viewMode: CalendarState['viewMode']; - locale?: string; - timeZone?: string; - calendar?: string; + locale?: Intl.UnicodeBCP47LocaleIdentifier; + timeZone?: Temporal.TimeZoneLike; + calendar?: Temporal.CalendarLike; } export interface CalendarApi { diff --git a/packages/time/src/utils/dateDefaults.ts b/packages/time/src/utils/dateDefaults.ts index 0dd3f98..0ac14ae 100644 --- a/packages/time/src/utils/dateDefaults.ts +++ b/packages/time/src/utils/dateDefaults.ts @@ -1,7 +1,9 @@ +import type { Temporal } from "@js-temporal/polyfill"; + export interface IDateDefaults { - calendar: string; - locale: string; - timeZone: string; + calendar: Temporal.CalendarLike; + locale: Intl.UnicodeBCP47LocaleIdentifier; + timeZone: Temporal.TimeZoneLike; } const { diff --git a/packages/time/src/utils/getFirstDayOfWeek.ts b/packages/time/src/utils/getFirstDayOfWeek.ts index 1b33e98..65f71e3 100644 --- a/packages/time/src/utils/getFirstDayOfWeek.ts +++ b/packages/time/src/utils/getFirstDayOfWeek.ts @@ -1,6 +1,6 @@ import { Temporal } from '@js-temporal/polyfill' -export const getFirstDayOfWeek = (currWeek: string, locale: string = 'en-US') => { +export const getFirstDayOfWeek = (currWeek: string, locale: Intl.UnicodeBCP47LocaleIdentifier | Intl.Locale = 'en-US') => { const date = Temporal.PlainDate.from(currWeek); const loc = new Intl.Locale(locale); const { firstDay } = loc.weekInfo || loc.getWeekInfo(); From 4131b8e56424f62f4bc4b983edb1fb515b204025 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 11:43:24 +0200 Subject: [PATCH 89/95] docs: jsdocs --- packages/time/src/core/calendar.ts | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 2a697b5..81b414b 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -14,38 +14,77 @@ import './weekInfoPolyfill'; export type { CalendarState, Event, Day } from '../calendar/types'; +/** + * Represents the configuration for the current viewing mode of a calendar, + * specifying the scale and unit of time. + */ export interface ViewMode { + /** The number of units for the view mode. */ value: number; + /** The unit of time that the calendar view should display (month, week, or day). */ unit: 'month' | 'week' | 'day'; } +/** + * Configuration options for initializing a CalendarCore instance, allowing customization + * of events, locale, time zone, and the calendar system. + * @template TEvent - Specifies the event type, extending a base Event type. + */ export interface CalendarCoreOptions { + /** An optional array of events to be handled by the calendar. */ events?: TEvent[] | null; + /** The initial view mode configuration of the calendar. */ viewMode: CalendarState['viewMode']; + /** Optional locale for date formatting. Uses a BCP 47 language tag. */ locale?: Intl.UnicodeBCP47LocaleIdentifier; + /** Optional time zone specification for the calendar. */ timeZone?: Temporal.TimeZoneLike; + /** Optional calendar system to be used. */ calendar?: Temporal.CalendarLike; } +/** + * The API surface provided by CalendarCore, allowing interaction with the calendar's state + * and manipulation of its settings and data. + * @template TEvent - The type of events handled by the calendar. + */ export interface CalendarApi { + /** The currently focused date period in the calendar. */ currentPeriod: CalendarState['currentPeriod']; + /** The current view mode of the calendar. */ viewMode: CalendarState['viewMode']; + /** The current date and time according to the calendar's time zone. */ currentTime: CalendarState['currentTime']; + /** An array of days, each potentially containing events. */ days: Array>; + /** An array of names for the days of the week, localized to the calendar's locale. */ daysNames: string[]; + /** Navigates to the previous period according to the current view mode. */ goToPreviousPeriod: () => void; + /** Navigates to the next period according to the current view mode. */ goToNextPeriod: () => void; + /** Resets the view to the current period based on today's date. */ goToCurrentPeriod: () => void; + /** Navigates to a specific date. */ goToSpecificPeriod: (date: Temporal.PlainDate) => void; + /** Changes the current view mode of the calendar. */ changeViewMode: (newViewMode: CalendarState['viewMode']) => void; + /** Retrieves styling properties for a specific event, identified by ID. */ getEventProps: (id: Event['id']) => { style: CSSProperties } | null; + /** Provides properties for the marker indicating the current time. */ getCurrentTimeMarkerProps: () => { style: CSSProperties; currentTime: string | undefined; }; + /** Groups days by a specified unit. */ groupDaysBy: (props: Omit, 'weekStartsOn'>) => (Day | null)[][]; } +/** + * Core functionality for a calendar system, managing the state and operations of the calendar, + * such as navigating through time periods, handling events, and adjusting settings. + * @template TEvent - The type of events managed by the calendar. + */ export class CalendarCore { store: Store; options: Required>; From b7f67f09e125371e18e4d8b7f5a4510a0d842ee5 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 11:58:02 +0200 Subject: [PATCH 90/95] refactor: types --- packages/time/src/calendar/types.ts | 2 +- packages/time/src/core/calendar.ts | 330 ++++++++++++++++------------ 2 files changed, 196 insertions(+), 136 deletions(-) diff --git a/packages/time/src/calendar/types.ts b/packages/time/src/calendar/types.ts index c3376b3..08ffe7e 100644 --- a/packages/time/src/calendar/types.ts +++ b/packages/time/src/calendar/types.ts @@ -7,7 +7,7 @@ export interface Event { title: string; } -export interface CalendarState { +export interface CalendarStore { currentPeriod: Temporal.PlainDate viewMode: { value: number diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 81b414b..50fe6b8 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -1,18 +1,18 @@ -import { Store } from '@tanstack/store'; -import { Temporal } from '@js-temporal/polyfill'; -import { getFirstDayOfMonth, getFirstDayOfWeek } from '../utils'; -import { generateDateRange } from '../calendar/generateDateRange'; -import { splitMultiDayEvents } from '../calendar/splitMultiDayEvents'; -import { getEventProps } from '../calendar/getEventProps'; -import { groupDaysBy } from '../calendar/groupDaysBy'; -import { getDateDefaults } from '../utils/dateDefaults'; -import type { Properties as CSSProperties } from 'csstype'; -import type { GroupDaysByProps } from '../calendar/groupDaysBy'; -import type { CalendarState, Day, Event } from '../calendar/types'; - -import './weekInfoPolyfill'; - -export type { CalendarState, Event, Day } from '../calendar/types'; +import { Store } from '@tanstack/store' +import { Temporal } from '@js-temporal/polyfill' +import { getFirstDayOfMonth, getFirstDayOfWeek } from '../utils' +import { generateDateRange } from '../calendar/generateDateRange' +import { splitMultiDayEvents } from '../calendar/splitMultiDayEvents' +import { getEventProps } from '../calendar/getEventProps' +import { groupDaysBy } from '../calendar/groupDaysBy' +import { getDateDefaults } from '../utils/dateDefaults' +import type { Properties as CSSProperties } from 'csstype' +import type { GroupDaysByProps } from '../calendar/groupDaysBy' +import type { CalendarStore, Day, Event } from '../calendar/types' + +import './weekInfoPolyfill' + +export type { CalendarStore, Event, Day } from '../calendar/types' /** * Represents the configuration for the current viewing mode of a calendar, @@ -20,9 +20,9 @@ export type { CalendarState, Event, Day } from '../calendar/types'; */ export interface ViewMode { /** The number of units for the view mode. */ - value: number; + value: number /** The unit of time that the calendar view should display (month, week, or day). */ - unit: 'month' | 'week' | 'day'; + unit: 'month' | 'week' | 'day' } /** @@ -32,15 +32,15 @@ export interface ViewMode { */ export interface CalendarCoreOptions { /** An optional array of events to be handled by the calendar. */ - events?: TEvent[] | null; + events?: TEvent[] | null /** The initial view mode configuration of the calendar. */ - viewMode: CalendarState['viewMode']; + viewMode: CalendarStore['viewMode'] /** Optional locale for date formatting. Uses a BCP 47 language tag. */ - locale?: Intl.UnicodeBCP47LocaleIdentifier; + locale?: Intl.UnicodeBCP47LocaleIdentifier /** Optional time zone specification for the calendar. */ - timeZone?: Temporal.TimeZoneLike; + timeZone?: Temporal.TimeZoneLike /** Optional calendar system to be used. */ - calendar?: Temporal.CalendarLike; + calendar?: Temporal.CalendarLike } /** @@ -48,245 +48,290 @@ export interface CalendarCoreOptions { * and manipulation of its settings and data. * @template TEvent - The type of events handled by the calendar. */ -export interface CalendarApi { - /** The currently focused date period in the calendar. */ - currentPeriod: CalendarState['currentPeriod']; - /** The current view mode of the calendar. */ - viewMode: CalendarState['viewMode']; - /** The current date and time according to the calendar's time zone. */ - currentTime: CalendarState['currentTime']; - /** An array of days, each potentially containing events. */ - days: Array>; - /** An array of names for the days of the week, localized to the calendar's locale. */ - daysNames: string[]; +interface CalendarActions { /** Navigates to the previous period according to the current view mode. */ - goToPreviousPeriod: () => void; + goToPreviousPeriod: () => void /** Navigates to the next period according to the current view mode. */ - goToNextPeriod: () => void; + goToNextPeriod: () => void /** Resets the view to the current period based on today's date. */ - goToCurrentPeriod: () => void; + goToCurrentPeriod: () => void /** Navigates to a specific date. */ - goToSpecificPeriod: (date: Temporal.PlainDate) => void; + goToSpecificPeriod: (date: Temporal.PlainDate) => void /** Changes the current view mode of the calendar. */ - changeViewMode: (newViewMode: CalendarState['viewMode']) => void; + changeViewMode: (newViewMode: CalendarStore['viewMode']) => void /** Retrieves styling properties for a specific event, identified by ID. */ - getEventProps: (id: Event['id']) => { style: CSSProperties } | null; + getEventProps: (id: Event['id']) => { style: CSSProperties } | null /** Provides properties for the marker indicating the current time. */ getCurrentTimeMarkerProps: () => { - style: CSSProperties; - currentTime: string | undefined; - }; + style: CSSProperties + currentTime: string | undefined + } /** Groups days by a specified unit. */ - groupDaysBy: (props: Omit, 'weekStartsOn'>) => (Day | null)[][]; + groupDaysBy: ( + props: Omit, 'weekStartsOn'>, + ) => (Day | null)[][] +} + +interface CalendarState { + /** The currently focused date period in the calendar. */ + currentPeriod: CalendarStore['currentPeriod'] + /** The current view mode of the calendar. */ + viewMode: CalendarStore['viewMode'] + /** The current date and time according to the calendar's time zone. */ + currentTime: CalendarStore['currentTime'] + /** An array of days, each potentially containing events. */ + days: Array> + /** An array of names for the days of the week, localized to the calendar's locale. */ + daysNames: string[] } +export interface CalendarApi + extends CalendarActions, + CalendarState {} + /** * Core functionality for a calendar system, managing the state and operations of the calendar, * such as navigating through time periods, handling events, and adjusting settings. * @template TEvent - The type of events managed by the calendar. */ -export class CalendarCore { - store: Store; - options: Required>; +export class CalendarCore + implements CalendarActions +{ + store: Store + options: Required> constructor(options: CalendarCoreOptions) { - const defaults = getDateDefaults(); + const defaults = getDateDefaults() this.options = { ...options, locale: options.locale || defaults.locale, timeZone: options.timeZone || defaults.timeZone, calendar: options.calendar || defaults.calendar, events: options.events || null, - }; - - this.store = new Store({ - currentPeriod: Temporal.Now.plainDateISO().withCalendar(this.options.calendar), + } + + this.store = new Store({ + currentPeriod: Temporal.Now.plainDateISO().withCalendar( + this.options.calendar, + ), viewMode: options.viewMode, currentTime: Temporal.Now.plainDateTimeISO(this.options.timeZone), - }); + }) } private getFirstDayOfMonth() { return getFirstDayOfMonth( - this.store.state.currentPeriod.toString({ calendarName: 'auto' }).substring(0, 7), - ); + this.store.state.currentPeriod + .toString({ calendarName: 'auto' }) + .substring(0, 7), + ) } private getFirstDayOfWeek() { - return getFirstDayOfWeek(this.store.state.currentPeriod.toString(), this.options.locale); + return getFirstDayOfWeek( + this.store.state.currentPeriod.toString(), + this.options.locale, + ) } private getCalendarDays() { const start = this.store.state.viewMode.unit === 'month' ? this.getFirstDayOfMonth().subtract({ - days: (this.getFirstDayOfMonth().dayOfWeek - (this.getFirstDayOfWeek().dayOfWeek + 1) + 7) % 7, + days: + (this.getFirstDayOfMonth().dayOfWeek - + (this.getFirstDayOfWeek().dayOfWeek + 1) + + 7) % + 7, }) - : this.store.state.currentPeriod; + : this.store.state.currentPeriod - let end; + let end switch (this.store.state.viewMode.unit) { case 'month': { const lastDayOfMonth = this.getFirstDayOfMonth() .add({ months: this.store.state.viewMode.value }) - .subtract({ days: 1 }); + .subtract({ days: 1 }) const lastDayOfMonthWeekDay = - (lastDayOfMonth.dayOfWeek - (this.getFirstDayOfWeek().dayOfWeek + 1) + 7) % 7; - end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }); - break; + (lastDayOfMonth.dayOfWeek - + (this.getFirstDayOfWeek().dayOfWeek + 1) + + 7) % + 7 + end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }) + break } case 'week': { - end = this.getFirstDayOfWeek().add({ days: 7 * this.store.state.viewMode.value - 1 }); - break; + end = this.getFirstDayOfWeek().add({ + days: 7 * this.store.state.viewMode.value - 1, + }) + break } case 'day': { - end = this.store.state.currentPeriod.add({ days: this.store.state.viewMode.value - 1 }); - break; + end = this.store.state.currentPeriod.add({ + days: this.store.state.viewMode.value - 1, + }) + break } } - const allDays = generateDateRange(start, end); - const startMonth = this.store.state.currentPeriod.month; + const allDays = generateDateRange(start, end) + const startMonth = this.store.state.currentPeriod.month const endMonth = this.store.state.currentPeriod.add({ months: this.store.state.viewMode.value - 1, - }).month; + }).month return allDays.filter( (day) => day.month >= startMonth && day.month <= endMonth, - ); + ) } private getEventMap() { - const map = new Map(); + const map = new Map() this.options.events?.forEach((event) => { - const eventStartDate = event.startDate instanceof Temporal.PlainDateTime ? event.startDate.toZonedDateTime(this.options.timeZone) : event.startDate; - const eventEndDate = event.endDate instanceof Temporal.PlainDateTime ? event.endDate.toZonedDateTime(this.options.timeZone) : event.endDate; - if ( - Temporal.ZonedDateTime.compare( - eventStartDate, - eventEndDate, - ) !== 0 - ) { - const splitEvents = splitMultiDayEvents(event, this.options.timeZone); + const eventStartDate = + event.startDate instanceof Temporal.PlainDateTime + ? event.startDate.toZonedDateTime(this.options.timeZone) + : event.startDate + const eventEndDate = + event.endDate instanceof Temporal.PlainDateTime + ? event.endDate.toZonedDateTime(this.options.timeZone) + : event.endDate + if (Temporal.ZonedDateTime.compare(eventStartDate, eventEndDate) !== 0) { + const splitEvents = splitMultiDayEvents( + event, + this.options.timeZone, + ) splitEvents.forEach((splitEvent) => { - const splitKey = splitEvent.startDate.toString().split('T')[0]; + const splitKey = splitEvent.startDate.toString().split('T')[0] if (splitKey) { - if (!map.has(splitKey)) map.set(splitKey, []); - map.get(splitKey)?.push(splitEvent); + if (!map.has(splitKey)) map.set(splitKey, []) + map.get(splitKey)?.push(splitEvent) } - }); + }) } else { - const eventKey = event.startDate.toString().split('T')[0]; + const eventKey = event.startDate.toString().split('T')[0] if (eventKey) { - if (!map.has(eventKey)) map.set(eventKey, []); - map.get(eventKey)?.push(event); + if (!map.has(eventKey)) map.set(eventKey, []) + map.get(eventKey)?.push(event) } } - }); - return map; + }) + return map } getDaysWithEvents() { - const calendarDays = this.getCalendarDays(); - const eventMap = this.getEventMap(); + const calendarDays = this.getCalendarDays() + const eventMap = this.getEventMap() return calendarDays.map((day) => { - const dayKey = day.toString(); - const dailyEvents = eventMap.get(dayKey) ?? []; + const dayKey = day.toString() + const dailyEvents = eventMap.get(dayKey) ?? [] const currentMonthRange = Array.from( { length: this.store.state.viewMode.value }, (_, i) => this.store.state.currentPeriod.add({ months: i }).month, - ); - const isInCurrentPeriod = currentMonthRange.includes(day.month); + ) + const isInCurrentPeriod = currentMonthRange.includes(day.month) return { date: day, events: dailyEvents, isToday: Temporal.PlainDate.compare(day, Temporal.Now.plainDateISO()) === 0, isInCurrentPeriod, - }; - }); + } + }) } getDaysNames() { - const baseDate = Temporal.PlainDate.from('2024-01-01'); + const baseDate = Temporal.PlainDate.from('2024-01-01') return Array.from({ length: 7 }).map((_, i) => baseDate .add({ days: (i + (this.getFirstDayOfWeek().dayOfWeek + 1)) % 7 }) .toLocaleString(this.options.locale, { weekday: 'short' }), - ); + ) } - changeViewMode(newViewMode: CalendarState['viewMode']) { + changeViewMode(newViewMode: CalendarStore['viewMode']) { this.store.setState((prev) => ({ ...prev, viewMode: newViewMode, - })); + })) } goToPreviousPeriod() { - const firstDayOfMonth = this.getFirstDayOfMonth(); - const firstDayOfWeek = this.getFirstDayOfWeek(); + const firstDayOfMonth = this.getFirstDayOfMonth() + const firstDayOfWeek = this.getFirstDayOfWeek() switch (this.store.state.viewMode.unit) { case 'month': { - const firstDayOfPrevMonth = firstDayOfMonth.subtract({ months: this.store.state.viewMode.value }); + const firstDayOfPrevMonth = firstDayOfMonth.subtract({ + months: this.store.state.viewMode.value, + }) this.store.setState((prev) => ({ ...prev, currentPeriod: firstDayOfPrevMonth, - })); - break; + })) + break } case 'week': { - const firstDayOfPrevWeek = firstDayOfWeek.subtract({ weeks: this.store.state.viewMode.value }); + const firstDayOfPrevWeek = firstDayOfWeek.subtract({ + weeks: this.store.state.viewMode.value, + }) this.store.setState((prev) => ({ ...prev, currentPeriod: firstDayOfPrevWeek, - })); - break; + })) + break } case 'day': { - const prevCustomStart = this.store.state.currentPeriod.subtract({ days: this.store.state.viewMode.value }); + const prevCustomStart = this.store.state.currentPeriod.subtract({ + days: this.store.state.viewMode.value, + }) this.store.setState((prev) => ({ ...prev, currentPeriod: prevCustomStart, - })); - break; + })) + break } } } goToNextPeriod() { - const firstDayOfMonth = this.getFirstDayOfMonth(); - const firstDayOfWeek = this.getFirstDayOfWeek(); + const firstDayOfMonth = this.getFirstDayOfMonth() + const firstDayOfWeek = this.getFirstDayOfWeek() switch (this.store.state.viewMode.unit) { case 'month': { - const firstDayOfNextMonth = firstDayOfMonth.add({ months: this.store.state.viewMode.value }); + const firstDayOfNextMonth = firstDayOfMonth.add({ + months: this.store.state.viewMode.value, + }) this.store.setState((prev) => ({ ...prev, currentPeriod: firstDayOfNextMonth, - })); - break; + })) + break } case 'week': { - const firstDayOfNextWeek = firstDayOfWeek.add({ weeks: this.store.state.viewMode.value }); + const firstDayOfNextWeek = firstDayOfWeek.add({ + weeks: this.store.state.viewMode.value, + }) this.store.setState((prev) => ({ ...prev, currentPeriod: firstDayOfNextWeek, - })); - break; + })) + break } case 'day': { - const nextCustomStart = this.store.state.currentPeriod.add({ days: this.store.state.viewMode.value }); + const nextCustomStart = this.store.state.currentPeriod.add({ + days: this.store.state.viewMode.value, + }) this.store.setState((prev) => ({ ...prev, currentPeriod: nextCustomStart, - })); - break; + })) + break } } } @@ -295,31 +340,34 @@ export class CalendarCore { this.store.setState((prev) => ({ ...prev, currentPeriod: Temporal.Now.plainDateISO(), - })); + })) } goToSpecificPeriod(date: Temporal.PlainDate) { this.store.setState((prev) => ({ ...prev, currentPeriod: date, - })); + })) } updateCurrentTime() { this.store.setState((prev) => ({ ...prev, currentTime: Temporal.Now.plainDateTimeISO(), - })); + })) } getEventProps(id: Event['id']) { - return getEventProps(this.getEventMap(), id, this.store.state); + return getEventProps(this.getEventMap(), id, this.store.state) } - getCurrentTimeMarkerProps(): { style: CSSProperties; currentTime: string | undefined } { - const { hour, minute } = this.store.state.currentTime; - const currentTimeInMinutes = hour * 60 + minute; - const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100; + getCurrentTimeMarkerProps(): { + style: CSSProperties + currentTime: string | undefined + } { + const { hour, minute } = this.store.state.currentTime + const currentTimeInMinutes = hour * 60 + minute + const percentageOfDay = (currentTimeInMinutes / (24 * 60)) * 100 return { style: { @@ -327,11 +375,23 @@ export class CalendarCore { top: `${percentageOfDay}%`, left: 0, }, - currentTime: this.store.state.currentTime.toString().split('T')[1]?.substring(0, 5), - }; + currentTime: this.store.state.currentTime + .toString() + .split('T')[1] + ?.substring(0, 5), + } } - groupDaysBy({ days, unit, fillMissingDays = true }: Omit, 'weekStartsOn'>) { - return groupDaysBy({ days, unit, fillMissingDays, weekStartsOn: this.getFirstDayOfWeek().dayOfWeek } as GroupDaysByProps); + groupDaysBy({ + days, + unit, + fillMissingDays = true, + }: Omit, 'weekStartsOn'>) { + return groupDaysBy({ + days, + unit, + fillMissingDays, + weekStartsOn: this.getFirstDayOfWeek().dayOfWeek, + } as GroupDaysByProps) } } From 4165b202724c3204a52c3a722b4f668a367ffdb7 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 12:05:25 +0200 Subject: [PATCH 91/95] refactor: types --- packages/react-time/src/useCalendar/useCalendar.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index 6dd3412..a9db19d 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -67,9 +67,7 @@ export const useCalendar = ( const groupDaysBy = useCallback((props) => calendarCore.groupDaysBy(props), [calendarCore]) return { - currentPeriod: state.currentPeriod, - viewMode: state.viewMode, - currentTime: state.currentTime, + ...state, days: calendarCore.getDaysWithEvents(), daysNames: calendarCore.getDaysNames(), goToPreviousPeriod, From 1b71c7eb681c59e159320f28ebd664c9ffff4004 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 14:00:21 +0200 Subject: [PATCH 92/95] test: useCalendar --- .../react-time/src/tests/useCalendar.test.tsx | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/react-time/src/tests/useCalendar.test.tsx b/packages/react-time/src/tests/useCalendar.test.tsx index ea50fed..ba20a20 100644 --- a/packages/react-time/src/tests/useCalendar.test.tsx +++ b/packages/react-time/src/tests/useCalendar.test.tsx @@ -241,20 +241,13 @@ describe('useCalendar', () => { expect(weeks.find((week) => week.some((day) => day?.isToday))?.find((day) => day?.isToday)?.date.toString()).toBe('2024-06-01'); }); - test('should return the correct day names based on weekStartsOn', () => { + test('should return the correct day names based on the locale', () => { const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US', weekStartsOn: 1 }) + useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US' }) ); const { daysNames } = result.current; expect(daysNames).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']); - - const { result: resultSundayStart } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US', weekStartsOn: 7 }) - ); - - const { daysNames: sundayDaysNames } = resultSundayStart.current; - expect(sundayDaysNames).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); }); test('should correctly mark days as in current period', () => { @@ -355,17 +348,4 @@ describe('useCalendar', () => { expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-27'); expect(weeks[4]?.[6]?.date.toString()).toBe('2024-06-30'); }); - - test('should group days by weeks correctly when weekStartsOn is Sunday', () => { - const { result } = renderHook(() => - useCalendar({ events, viewMode: { value: 1, unit: 'month' }, locale: 'en-US', weekStartsOn: 7 }) - ); - - const { days, groupDaysBy } = result.current; - const weeks = groupDaysBy({ days, unit: 'week' }); - - expect(weeks).toHaveLength(6); - expect(weeks[0]?.[0]?.date.toString()).toBe('2024-05-26'); - expect(weeks[4]?.[6]?.date.toString()).toBe('2024-06-29'); - }); }); From 604c8bb7f611ccdada6b6a951600793d1bfd0a5f Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 16:41:18 +0200 Subject: [PATCH 93/95] refactor: useIsomorphicLayoutEffect --- packages/react-time/src/useCalendar/useCalendar.ts | 5 +++-- packages/react-time/src/utils/index.ts | 1 + packages/react-time/src/utils/useIsomorphicLayoutEffect.ts | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 packages/react-time/src/utils/index.ts create mode 100644 packages/react-time/src/utils/useIsomorphicLayoutEffect.ts diff --git a/packages/react-time/src/useCalendar/useCalendar.ts b/packages/react-time/src/useCalendar/useCalendar.ts index a9db19d..84a236c 100644 --- a/packages/react-time/src/useCalendar/useCalendar.ts +++ b/packages/react-time/src/useCalendar/useCalendar.ts @@ -1,7 +1,8 @@ -import { useCallback, useEffect, useRef, useState, useTransition } from 'react' +import { useCallback, useRef, useState, useTransition } from 'react' import { useStore } from '@tanstack/react-store' import { Temporal } from '@js-temporal/polyfill' import { CalendarCore, type Event } from '@tanstack/time' +import { useIsomorphicLayoutEffect } from '../utils' import type { CalendarApi, CalendarCoreOptions } from '@tanstack/time' export const useCalendar = ( @@ -16,7 +17,7 @@ export const useCalendar = ( calendarCore.updateCurrentTime() }, [calendarCore]) - useEffect(() => { + useIsomorphicLayoutEffect(() => { if (currentTimeInterval.current) clearTimeout(currentTimeInterval.current) const now = Temporal.Now.plainDateTimeISO() diff --git a/packages/react-time/src/utils/index.ts b/packages/react-time/src/utils/index.ts new file mode 100644 index 0000000..881ccca --- /dev/null +++ b/packages/react-time/src/utils/index.ts @@ -0,0 +1 @@ +export * from './useIsomorphicLayoutEffect' diff --git a/packages/react-time/src/utils/useIsomorphicLayoutEffect.ts b/packages/react-time/src/utils/useIsomorphicLayoutEffect.ts new file mode 100644 index 0000000..287b88a --- /dev/null +++ b/packages/react-time/src/utils/useIsomorphicLayoutEffect.ts @@ -0,0 +1,4 @@ +import { useEffect, useLayoutEffect } from "react"; + +export const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect \ No newline at end of file From 4ee4be477563bead4739d00e5918e743e9c14102 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 21:38:41 +0200 Subject: [PATCH 94/95] chore: resolve conflicts --- packages/time/src/core/calendar.ts | 2 +- packages/time/src/core/date-picker.ts | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/time/src/core/calendar.ts b/packages/time/src/core/calendar.ts index 50fe6b8..ecbc94a 100644 --- a/packages/time/src/core/calendar.ts +++ b/packages/time/src/core/calendar.ts @@ -94,7 +94,7 @@ export interface CalendarApi * such as navigating through time periods, handling events, and adjusting settings. * @template TEvent - The type of events managed by the calendar. */ -export class CalendarCore +export class CalendarCore implements CalendarActions { store: Store diff --git a/packages/time/src/core/date-picker.ts b/packages/time/src/core/date-picker.ts index b8e8080..14c7923 100644 --- a/packages/time/src/core/date-picker.ts +++ b/packages/time/src/core/date-picker.ts @@ -1,27 +1,41 @@ import { Store } from '@tanstack/store'; import { Temporal } from '@js-temporal/polyfill'; +import { getDateDefaults } from '../utils/dateDefaults'; import { CalendarCore } from './calendar'; -import type { CalendarCoreOptions, CalendarState } from './calendar'; +import type { CalendarCoreOptions, CalendarStore } from './calendar'; export interface DatePickerOptions extends CalendarCoreOptions { - minDate?: Temporal.PlainDate; - maxDate?: Temporal.PlainDate; + minDate?: Temporal.PlainDate | null; + maxDate?: Temporal.PlainDate | null; multiple?: boolean; range?: boolean; selectedDates?: Temporal.PlainDate[]; } -export interface DatePickerCoreState extends CalendarState { +export interface DatePickerCoreState extends CalendarStore { selectedDates: Map; } export class DatePickerCore extends CalendarCore { datePickerStore: Store; - options: DatePickerOptions; + options: Required; constructor(options: DatePickerOptions) { super(options); - this.options = options; + const defaults = getDateDefaults() + + this.options = { + ...options, + multiple: options.multiple ?? false, + range: options.range ?? false, + minDate: options.minDate ?? null, + maxDate: options.maxDate ?? null, + selectedDates: options.selectedDates ?? [], + events: options.events ?? [], + locale: options.locale ?? defaults.locale, + timeZone: options.timeZone ?? defaults.timeZone, + calendar: options.calendar ?? defaults.calendar, + } this.datePickerStore = new Store({ ...this.store.state, selectedDates: new Map(options.selectedDates?.map(date => [date.toString(), date]) ?? []), From bd1d3e377db193b2ba93970caff7500c91e81660 Mon Sep 17 00:00:00 2001 From: Bart Krakowski Date: Tue, 25 Jun 2024 21:46:58 +0200 Subject: [PATCH 95/95] docs: jsdocs --- packages/time/src/core/date-picker.ts | 76 +++++++++++++++++---------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/time/src/core/date-picker.ts b/packages/time/src/core/date-picker.ts index 14c7923..a238f2c 100644 --- a/packages/time/src/core/date-picker.ts +++ b/packages/time/src/core/date-picker.ts @@ -1,27 +1,45 @@ -import { Store } from '@tanstack/store'; -import { Temporal } from '@js-temporal/polyfill'; -import { getDateDefaults } from '../utils/dateDefaults'; -import { CalendarCore } from './calendar'; -import type { CalendarCoreOptions, CalendarStore } from './calendar'; +import { Store } from '@tanstack/store' +import { Temporal } from '@js-temporal/polyfill' +import { getDateDefaults } from '../utils/dateDefaults' +import { CalendarCore } from './calendar' +import type { CalendarCoreOptions, CalendarStore } from './calendar' export interface DatePickerOptions extends CalendarCoreOptions { - minDate?: Temporal.PlainDate | null; - maxDate?: Temporal.PlainDate | null; - multiple?: boolean; - range?: boolean; - selectedDates?: Temporal.PlainDate[]; + /** + * The earliest date that can be selected. Null if no minimum constraint. + */ + minDate?: Temporal.PlainDate | null + /** + * The latest date that can be selected. Null if no maximum constraint. + */ + maxDate?: Temporal.PlainDate | null + /** + * Allows selection of multiple dates. + */ + multiple?: boolean + /** + * Allows selection of a range of dates. + */ + range?: boolean + /** + * Initial set of selected dates. + */ + selectedDates?: Temporal.PlainDate[] } export interface DatePickerCoreState extends CalendarStore { - selectedDates: Map; + /** + * A map of selected dates, keyed by their string representation. + */ + selectedDates: Map } export class DatePickerCore extends CalendarCore { - datePickerStore: Store; - options: Required; + datePickerStore: Store + options: Required constructor(options: DatePickerOptions) { - super(options); + super(options) const defaults = getDateDefaults() this.options = { @@ -38,38 +56,40 @@ export class DatePickerCore extends CalendarCore { } this.datePickerStore = new Store({ ...this.store.state, - selectedDates: new Map(options.selectedDates?.map(date => [date.toString(), date]) ?? []), - }); + selectedDates: new Map( + options.selectedDates?.map((date) => [date.toString(), date]) ?? [], + ), + }) } getSelectedDates() { - return Array.from(this.datePickerStore.state.selectedDates.values()); + return Array.from(this.datePickerStore.state.selectedDates.values()) } selectDate(date: Temporal.PlainDate) { - const { multiple, range, minDate, maxDate } = this.options; + const { multiple, range, minDate, maxDate } = this.options - if (minDate && Temporal.PlainDate.compare(date, minDate) < 0) return; - if (maxDate && Temporal.PlainDate.compare(date, maxDate) > 0) return; + if (minDate && Temporal.PlainDate.compare(date, minDate) < 0) return + if (maxDate && Temporal.PlainDate.compare(date, maxDate) > 0) return - const selectedDates = new Map(this.datePickerStore.state.selectedDates); + const selectedDates = new Map(this.datePickerStore.state.selectedDates) if (range && selectedDates.size === 1) { - selectedDates.set(date.toString(), date); + selectedDates.set(date.toString(), date) } else if (multiple) { if (selectedDates.has(date.toString())) { - selectedDates.delete(date.toString()); + selectedDates.delete(date.toString()) } else { - selectedDates.set(date.toString(), date); + selectedDates.set(date.toString(), date) } } else { - selectedDates.clear(); - selectedDates.set(date.toString(), date); + selectedDates.clear() + selectedDates.set(date.toString(), date) } - this.datePickerStore.setState(prev => ({ + this.datePickerStore.setState((prev) => ({ ...prev, selectedDates, - })); + })) } }