diff --git a/src/apps/stable/features/libraries/api/useLatestMedia.ts b/src/apps/stable/features/libraries/api/useLatestMedia.ts new file mode 100644 index 0000000000..f70b7e86e7 --- /dev/null +++ b/src/apps/stable/features/libraries/api/useLatestMedia.ts @@ -0,0 +1,36 @@ +import type { Api } from '@jellyfin/sdk/lib/api'; +import type { UserLibraryApiGetLatestMediaRequest } from '@jellyfin/sdk/lib/generated-client/api/user-library-api'; +import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; +import { queryOptions, useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { useApi } from 'hooks/useApi'; + +const fetchLatestMedia = async ( + api: Api, + params?: UserLibraryApiGetLatestMediaRequest, + options?: AxiosRequestConfig +) => { + const response = await getUserLibraryApi(api) + .getLatestMedia(params, options); + return response.data; +}; + +export const getLatestMediaQuery = ( + api?: Api, + params?: UserLibraryApiGetLatestMediaRequest +) => queryOptions({ + queryKey: [ 'User', params?.userId, 'LatestMedia', params ], + queryFn: ({ signal }) => fetchLatestMedia(api!, params, { signal }), + enabled: !!api +}); + +export const useLatestMedia = ( + params?: UserLibraryApiGetLatestMediaRequest +) => { + const { api, user } = useApi(); + return useQuery(getLatestMediaQuery(api, { + ...params, + userId: params?.userId || user?.Id + })); +}; diff --git a/src/apps/stable/features/libraries/api/useNextUp.ts b/src/apps/stable/features/libraries/api/useNextUp.ts new file mode 100644 index 0000000000..5d0c81f66b --- /dev/null +++ b/src/apps/stable/features/libraries/api/useNextUp.ts @@ -0,0 +1,36 @@ +import type { Api } from '@jellyfin/sdk/lib/api'; +import type { TvShowsApiGetNextUpRequest } from '@jellyfin/sdk/lib/generated-client/api/tv-shows-api'; +import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api'; +import { queryOptions, useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { useApi } from 'hooks/useApi'; + +const fetchNextUp = async ( + api: Api, + params?: TvShowsApiGetNextUpRequest, + options?: AxiosRequestConfig +) => { + const response = await getTvShowsApi(api) + .getNextUp(params, options); + return response.data; +}; + +export const getNextUpQuery = ( + api?: Api, + params?: TvShowsApiGetNextUpRequest +) => queryOptions({ + queryKey: [ 'User', params?.userId, 'NextUp', params ], + queryFn: ({ signal }) => fetchNextUp(api!, params, { signal }), + enabled: !!api +}); + +export const useNextUp = ( + params?: TvShowsApiGetNextUpRequest +) => { + const { api, user } = useApi(); + return useQuery(getNextUpQuery(api, { + ...params, + userId: params?.userId || user?.Id + })); +}; diff --git a/src/apps/stable/features/libraries/api/useResumeItems.ts b/src/apps/stable/features/libraries/api/useResumeItems.ts new file mode 100644 index 0000000000..43488462dd --- /dev/null +++ b/src/apps/stable/features/libraries/api/useResumeItems.ts @@ -0,0 +1,36 @@ +import type { Api } from '@jellyfin/sdk/lib/api'; +import type { ItemsApiGetResumeItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api'; +import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; +import { queryOptions, useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { useApi } from 'hooks/useApi'; + +const fetchResumeItems = async ( + api: Api, + params?: ItemsApiGetResumeItemsRequest, + options?: AxiosRequestConfig +) => { + const response = await getItemsApi(api) + .getResumeItems(params, options); + return response.data; +}; + +export const getResumeItemsQuery = ( + api?: Api, + params?: ItemsApiGetResumeItemsRequest +) => queryOptions({ + queryKey: [ 'User', params?.userId, 'ResumeItems', params ], + queryFn: ({ signal }) => fetchResumeItems(api!, params, { signal }), + enabled: !!api +}); + +export const useResumeItems = ( + params?: ItemsApiGetResumeItemsRequest +) => { + const { api, user } = useApi(); + return useQuery(getResumeItemsQuery(api, { + ...params, + userId: params?.userId || user?.Id + })); +}; diff --git a/src/apps/stable/features/liveTv/api/useRecommendedPrograms.ts b/src/apps/stable/features/liveTv/api/useRecommendedPrograms.ts new file mode 100644 index 0000000000..e9f6b077d4 --- /dev/null +++ b/src/apps/stable/features/liveTv/api/useRecommendedPrograms.ts @@ -0,0 +1,36 @@ +import type { Api } from '@jellyfin/sdk/lib/api'; +import type { LiveTvApiGetRecommendedProgramsRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api'; +import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; +import { queryOptions, useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { useApi } from 'hooks/useApi'; + +const fetchRecommendedPrograms = async ( + api: Api, + params?: LiveTvApiGetRecommendedProgramsRequest, + options?: AxiosRequestConfig +) => { + const response = await getLiveTvApi(api) + .getRecommendedPrograms(params, options); + return response.data; +}; + +export const getRecommendedProgramsQuery = ( + api?: Api, + params?: LiveTvApiGetRecommendedProgramsRequest +) => queryOptions({ + queryKey: [ 'User', params?.userId, 'RecommendedPrograms', params ], + queryFn: ({ signal }) => fetchRecommendedPrograms(api!, params, { signal }), + enabled: !!api +}); + +export const useRecommendedPrograms = ( + params?: LiveTvApiGetRecommendedProgramsRequest +) => { + const { api, user } = useApi(); + return useQuery(getRecommendedProgramsQuery(api, { + ...params, + userId: params?.userId || user?.Id + })); +}; diff --git a/src/apps/stable/features/liveTv/api/useRecordings.ts b/src/apps/stable/features/liveTv/api/useRecordings.ts new file mode 100644 index 0000000000..09d4b5a8b7 --- /dev/null +++ b/src/apps/stable/features/liveTv/api/useRecordings.ts @@ -0,0 +1,35 @@ +import type { Api } from '@jellyfin/sdk'; +import type { LiveTvApiGetRecordingsRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api'; +import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; +import { queryOptions, useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { useApi } from 'hooks/useApi'; + +const fetchRecordings = async ( + api: Api, + params?: LiveTvApiGetRecordingsRequest, + options?: AxiosRequestConfig +) => { + const response = await getLiveTvApi(api).getRecordings(params, options); + return response.data; +}; + +export const getRecordingsQuery = ( + api?: Api, + params?: LiveTvApiGetRecordingsRequest +) => queryOptions({ + queryKey: [ 'User', params?.userId, 'Recordings', params ], + queryFn: ({ signal }) => fetchRecordings(api!, params, { signal }), + enabled: !!api +}); + +export const useRecordings = ( + params?: LiveTvApiGetRecordingsRequest +) => { + const { api, user } = useApi(); + return useQuery(getRecordingsQuery(api, { + ...params, + userId: params?.userId || user?.Id + })); +}; diff --git a/src/components/homesections/sections/activeRecordings.ts b/src/components/homesections/sections/activeRecordings.ts index 72ab3dfe68..7bac05b15f 100644 --- a/src/components/homesections/sections/activeRecordings.ts +++ b/src/components/homesections/sections/activeRecordings.ts @@ -1,9 +1,13 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import type { ApiClient } from 'jellyfin-apiclient'; +import { getRecordingsQuery } from 'apps/stable/features/liveTv/api/useRecordings'; import cardBuilder from 'components/cardbuilder/cardBuilder'; import globalize from 'lib/globalize'; import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { queryClient } from 'utils/query/queryClient'; import type { SectionContainerElement, SectionOptions } from './section'; @@ -14,14 +18,14 @@ function getLatestRecordingsFetchFn( ) { return function () { const apiClient = ServerConnections.getApiClient(serverId); - return apiClient.getLiveTvRecordings({ + return queryClient.fetchQuery(getRecordingsQuery(toApi(apiClient), { userId: apiClient.getCurrentUserId(), - Limit: enableOverflow ? 12 : 5, - Fields: 'PrimaryImageAspectRatio', - EnableTotalRecordCount: false, - IsLibraryItem: activeRecordingsOnly ? null : false, - IsInProgress: activeRecordingsOnly ? true : null - }); + limit: enableOverflow ? 12 : 5, + fields: [ ItemFields.PrimaryImageAspectRatio ], + enableTotalRecordCount: false, + isLibraryItem: activeRecordingsOnly ? undefined : false, + isInProgress: activeRecordingsOnly ? true : undefined + })); }; } diff --git a/src/components/homesections/sections/liveTv.ts b/src/components/homesections/sections/liveTv.ts index 8ecc032b35..47be8f5b93 100644 --- a/src/components/homesections/sections/liveTv.ts +++ b/src/components/homesections/sections/liveTv.ts @@ -1,4 +1,6 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; import type { ApiClient } from 'jellyfin-apiclient'; @@ -7,24 +9,33 @@ import cardBuilder from 'components/cardbuilder/cardBuilder'; import { getBackdropShape } from 'components/cardbuilder/utils/shape'; import layoutManager from 'components/layoutManager'; import globalize from 'lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; import type { SectionContainerElement, SectionOptions } from './section'; +import { queryClient } from 'utils/query/queryClient'; +import { getRecommendedProgramsQuery } from 'apps/stable/features/liveTv/api/useRecommendedPrograms'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; function getOnNowFetchFn( - serverId: string + apiClient: ApiClient ) { return function () { - const apiClient = ServerConnections.getApiClient(serverId); - return apiClient.getLiveTvRecommendedPrograms({ - userId: apiClient.getCurrentUserId(), - IsAiring: true, - limit: 24, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Thumb,Backdrop', - EnableTotalRecordCount: false, - Fields: 'ChannelInfo,PrimaryImageAspectRatio' - }); + return queryClient + .fetchQuery(getRecommendedProgramsQuery(toApi(apiClient), { + userId: apiClient.getCurrentUserId(), + isAiring: true, + limit: 24, + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Thumb, + ImageType.Backdrop + ], + enableTotalRecordCount: false, + fields: [ + ItemFields.ChannelInfo, + ItemFields.PrimaryImageAspectRatio + ] + })); }; } @@ -56,16 +67,17 @@ function getOnNowItemsHtmlFn( function buildSection( elem: HTMLElement, - serverId: string, + apiClient: ApiClient, options: SectionOptions ) { - let html = ''; + const serverId = apiClient.serverId(); elem.classList.remove('padded-left'); elem.classList.remove('padded-right'); elem.classList.remove('padded-bottom'); elem.classList.remove('verticalSection'); + let html = ''; html += '
'; html += '
'; html += '

' + globalize.translate('LiveTV') + '

'; @@ -151,7 +163,7 @@ function buildSection( const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); if (!itemsContainer) return; itemsContainer.parentContainer = elem; - itemsContainer.fetchData = getOnNowFetchFn(serverId); + itemsContainer.fetchData = getOnNowFetchFn(apiClient); itemsContainer.getItemsHtml = getOnNowItemsHtmlFn(options); } @@ -165,17 +177,26 @@ export function loadLiveTV( return Promise.resolve(); } - return apiClient.getLiveTvRecommendedPrograms({ - userId: apiClient.getCurrentUserId(), - IsAiring: true, - limit: 1, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Thumb,Backdrop', - EnableTotalRecordCount: false, - Fields: 'ChannelInfo,PrimaryImageAspectRatio' - }).then(function (result) { - if (result.Items?.length) { - buildSection(elem, apiClient.serverId(), options); - } - }); + return queryClient + .fetchQuery(getRecommendedProgramsQuery(toApi(apiClient), { + userId: apiClient.getCurrentUserId(), + isAiring: true, + limit: 1, + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Thumb, + ImageType.Backdrop + ], + enableTotalRecordCount: false, + fields: [ + ItemFields.ChannelInfo, + ItemFields.PrimaryImageAspectRatio + ] + })) + .then(programs => { + if (programs.Items?.length) { + buildSection(elem, apiClient, options); + } + }); } diff --git a/src/components/homesections/sections/nextUp.ts b/src/components/homesections/sections/nextUp.ts index 1f8840bdb7..a4358bf1b3 100644 --- a/src/components/homesections/sections/nextUp.ts +++ b/src/components/homesections/sections/nextUp.ts @@ -1,36 +1,50 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import type { ApiClient } from 'jellyfin-apiclient'; +import { getNextUpQuery } from 'apps/stable/features/libraries/api/useNextUp'; import cardBuilder from 'components/cardbuilder/cardBuilder'; import { getBackdropShape } from 'components/cardbuilder/utils/shape'; import layoutManager from 'components/layoutManager'; import { appRouter } from 'components/router/appRouter'; import globalize from 'lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; import type { UserSettings } from 'scripts/settings/userSettings'; +import { toIsoDateOnlyString } from 'utils/date'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { queryClient } from 'utils/query/queryClient'; import type { SectionContainerElement, SectionOptions } from './section'; function getNextUpFetchFn( - serverId: string, + apiClient: ApiClient, userSettings: UserSettings, { enableOverflow }: SectionOptions ) { return function () { - const apiClient = ServerConnections.getApiClient(serverId); const oldestDateForNextUp = new Date(); oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp()); - return apiClient.getNextUpEpisodes({ - Limit: enableOverflow ? 24 : 15, - Fields: 'PrimaryImageAspectRatio,DateCreated,Path,MediaSourceCount', - UserId: apiClient.getCurrentUserId(), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - EnableTotalRecordCount: false, - NextUpDateCutoff: oldestDateForNextUp.toISOString(), - EnableResumable: false, - EnableRewatching: userSettings.enableRewatchingInNextUp() - }); + return queryClient + .fetchQuery(getNextUpQuery(toApi(apiClient), { + userId: apiClient.getCurrentUserId(), + limit: enableOverflow ? 24 : 15, + fields: [ + ItemFields.PrimaryImageAspectRatio, + ItemFields.DateCreated, + ItemFields.Path, + ItemFields.MediaSourceCount + ], + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Thumb + ], + enableTotalRecordCount: false, + nextUpDateCutoff: toIsoDateOnlyString(oldestDateForNextUp), + enableResumable: false, + enableRewatching: userSettings.enableRewatchingInNextUp() + })); }; } @@ -100,7 +114,7 @@ export function loadNextUp( const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); if (!itemsContainer) return; - itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options); + itemsContainer.fetchData = getNextUpFetchFn(apiClient, userSettings, options); itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options); itemsContainer.parentContainer = elem; } diff --git a/src/components/homesections/sections/recentlyAdded.ts b/src/components/homesections/sections/recentlyAdded.ts index 4da428978c..66bd61aa65 100644 --- a/src/components/homesections/sections/recentlyAdded.ts +++ b/src/components/homesections/sections/recentlyAdded.ts @@ -1,27 +1,31 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; -import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; import escapeHtml from 'escape-html'; import type { ApiClient } from 'jellyfin-apiclient'; +import { getLatestMediaQuery } from 'apps/stable/features/libraries/api/useLatestMedia'; import cardBuilder from 'components/cardbuilder/cardBuilder'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'components/cardbuilder/utils/shape'; import layoutManager from 'components/layoutManager'; import { appRouter } from 'components/router/appRouter'; import globalize from 'lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { queryClient } from 'utils/query/queryClient'; import type { SectionContainerElement, SectionOptions } from './section'; function getFetchLatestItemsFn( - serverId: string, + apiClient: ApiClient, + user: UserDto | undefined, parentId: string | undefined, collectionType: string | null | undefined, { enableOverflow }: SectionOptions ) { return function () { - const apiClient = ServerConnections.getApiClient(serverId); let limit = 16; if (enableOverflow) { @@ -37,14 +41,23 @@ function getFetchLatestItemsFn( } const options = { - Limit: limit, - Fields: 'PrimaryImageAspectRatio,Path', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - ParentId: parentId + userId: user?.Id, + limit, + fields: [ + ItemFields.PrimaryImageAspectRatio, + ItemFields.Path + ], + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Thumb + ], + parentId }; - return apiClient.getLatestItems(options); + return queryClient + .fetchQuery(getLatestMediaQuery(toApi(apiClient), options)); }; } @@ -124,7 +137,7 @@ function renderLatestSection( const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); if (!itemsContainer) return; - itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType, options); + itemsContainer.fetchData = getFetchLatestItemsFn(apiClient, user, parent.Id, parent.CollectionType, options); itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType, options); itemsContainer.parentContainer = elem; } diff --git a/src/components/homesections/sections/resume.ts b/src/components/homesections/sections/resume.ts index 01c7e5b556..733f86eed2 100644 --- a/src/components/homesections/sections/resume.ts +++ b/src/components/homesections/sections/resume.ts @@ -1,12 +1,16 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; -import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; +import type { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; import type { ApiClient } from 'jellyfin-apiclient'; +import { getResumeItemsQuery } from 'apps/stable/features/libraries/api/useResumeItems'; import cardBuilder from 'components/cardbuilder/cardBuilder'; import { getBackdropShape, getPortraitShape } from 'components/cardbuilder/utils/shape'; import globalize from 'lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { queryClient } from 'utils/query/queryClient'; import type { UserSettings } from 'scripts/settings/userSettings'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; import type { SectionContainerElement, SectionOptions } from './section'; @@ -16,32 +20,35 @@ const dataMonitorHints: Record = { }; function getItemsToResumeFn( - mediaType: BaseItemKind, - serverId: string, + apiClient: ApiClient, + mediaType: MediaType, { enableOverflow }: SectionOptions ) { return function () { - const apiClient = ServerConnections.getApiClient(serverId); - const limit = enableOverflow ? 12 : 5; const options = { - Limit: limit, - Recursive: true, - Fields: 'PrimaryImageAspectRatio', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - EnableTotalRecordCount: false, - MediaTypes: mediaType + userId: apiClient.getCurrentUserId(), + limit, + fields: [ ItemFields.PrimaryImageAspectRatio ], + imageTypeLimit: 1, + enableImageTypes: [ + ImageType.Primary, + ImageType.Backdrop, + ImageType.Thumb + ], + enableTotalRecordCount: false, + mediaTypes: [ mediaType ] }; - return apiClient.getResumableItems(apiClient.getCurrentUserId(), options); + return queryClient + .fetchQuery(getResumeItemsQuery(toApi(apiClient), options)); }; } function getItemsToResumeHtmlFn( useEpisodeImages: boolean, - mediaType: BaseItemKind, + mediaType: MediaType, { enableOverflow }: SectionOptions ) { return function (items: BaseItemDto[]) { @@ -73,7 +80,7 @@ export function loadResume( elem: HTMLElement, apiClient: ApiClient, titleLabel: string, - mediaType: BaseItemKind, + mediaType: MediaType, userSettings: UserSettings, options: SectionOptions ) { @@ -99,7 +106,7 @@ export function loadResume( const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); if (!itemsContainer) return; - itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId(), options); + itemsContainer.fetchData = getItemsToResumeFn(apiClient, mediaType, options); itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType, options); itemsContainer.parentContainer = elem; } diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000000..ec4e41e6f9 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,4 @@ +/** Converts a Date object to an ISO date string (YYYY-MM-DD). */ +export const toIsoDateOnlyString = (date: Date) => { + return date.toISOString().split('T')[0]; +};