@carlonicora/nextjs-jsonapi 6.24.0 → 6.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BlockNoteEditor-MONT74RQ.js → BlockNoteEditor-TNX77GCD.js} +28 -25
- package/dist/BlockNoteEditor-TNX77GCD.js.map +1 -0
- package/dist/{BlockNoteEditor-ENDOVTAW.mjs → BlockNoteEditor-Z5LDF47Z.mjs} +13 -10
- package/dist/{BlockNoteEditor-ENDOVTAW.mjs.map → BlockNoteEditor-Z5LDF47Z.mjs.map} +1 -1
- package/dist/{BlockNoteViewer-K2HBF3IP.js → BlockNoteViewer-6KQJODNF.js} +9 -7
- package/dist/BlockNoteViewer-6KQJODNF.js.map +1 -0
- package/dist/{BlockNoteViewer-2TJZ6JET.mjs → BlockNoteViewer-TQ452ZYW.mjs} +8 -6
- package/dist/{BlockNoteViewer-2TJZ6JET.mjs.map → BlockNoteViewer-TQ452ZYW.mjs.map} +1 -1
- package/dist/JsonApiRequest-OKNQZLS6.js +26 -0
- package/dist/{JsonApiRequest-KC6DOHKT.js.map → JsonApiRequest-OKNQZLS6.js.map} +1 -1
- package/dist/{JsonApiRequest-ADLBJ5RX.mjs → JsonApiRequest-WNPDS356.mjs} +2 -2
- package/dist/billing/index.js +504 -517
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +20 -33
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-VNUT2D27.mjs → chunk-2PWYG2WJ.mjs} +2 -2
- package/dist/{chunk-W4ML6D2X.mjs → chunk-3U7YJ73T.mjs} +7 -7
- package/dist/{chunk-BQ2WAG4K.js → chunk-3VNDFKT7.js} +19 -19
- package/dist/{chunk-BQ2WAG4K.js.map → chunk-3VNDFKT7.js.map} +1 -1
- package/dist/{chunk-BE6UMM5X.mjs → chunk-47HNJQ7U.mjs} +3 -3
- package/dist/chunk-5T2AQMQ2.mjs +90 -0
- package/dist/chunk-5T2AQMQ2.mjs.map +1 -0
- package/dist/{chunk-A5M3MORY.js → chunk-5WM72MLE.js} +3 -3
- package/dist/{chunk-A5M3MORY.js.map → chunk-5WM72MLE.js.map} +1 -1
- package/dist/{chunk-S7SPAC6X.mjs → chunk-73NKUBMP.mjs} +3 -3
- package/dist/chunk-743M4TIB.mjs +76 -0
- package/dist/chunk-743M4TIB.mjs.map +1 -0
- package/dist/{chunk-HN7L6RRW.js → chunk-CSBQAPHD.js} +903 -955
- package/dist/chunk-CSBQAPHD.js.map +1 -0
- package/dist/{chunk-YTDGYI62.mjs → chunk-DWPHHUMV.mjs} +3 -3
- package/dist/{chunk-53SD4YRF.js → chunk-FBHUUPRV.js} +5 -5
- package/dist/{chunk-53SD4YRF.js.map → chunk-FBHUUPRV.js.map} +1 -1
- package/dist/{chunk-T6G2F2BK.mjs → chunk-HJT7TXGG.mjs} +16 -68
- package/dist/chunk-HJT7TXGG.mjs.map +1 -0
- package/dist/chunk-HROKKUBQ.js +156 -0
- package/dist/chunk-HROKKUBQ.js.map +1 -0
- package/dist/{chunk-TMV7TL7O.mjs → chunk-JELEQV5T.mjs} +8 -6
- package/dist/{chunk-TMV7TL7O.mjs.map → chunk-JELEQV5T.mjs.map} +1 -1
- package/dist/{chunk-TNTCLVUR.js → chunk-JGL4P3DS.js} +17 -15
- package/dist/chunk-JGL4P3DS.js.map +1 -0
- package/dist/{chunk-7OIWOPM5.mjs → chunk-K7KLUYS7.mjs} +4 -3
- package/dist/chunk-K7KLUYS7.mjs.map +1 -0
- package/dist/{chunk-OZADFYE7.mjs → chunk-KVYYPKRO.mjs} +308 -241
- package/dist/chunk-KVYYPKRO.mjs.map +1 -0
- package/dist/{chunk-RCOKQ3R4.js → chunk-LNO2JLU3.js} +2 -2
- package/dist/{chunk-RCOKQ3R4.js.map → chunk-LNO2JLU3.js.map} +1 -1
- package/dist/{chunk-BDWV4KYR.js → chunk-M6SNTMK6.js} +1386 -1319
- package/dist/chunk-M6SNTMK6.js.map +1 -0
- package/dist/{chunk-GMMDXINC.js → chunk-N3NQS7X6.js} +12 -12
- package/dist/{chunk-GMMDXINC.js.map → chunk-N3NQS7X6.js.map} +1 -1
- package/dist/chunk-NP3AFULU.js +76 -0
- package/dist/chunk-NP3AFULU.js.map +1 -0
- package/dist/{chunk-SO3YUCL6.js → chunk-OOM54Z5Z.js} +6 -5
- package/dist/chunk-OOM54Z5Z.js.map +1 -0
- package/dist/{chunk-YSE2Z4RB.js → chunk-PQQFOONG.js} +12 -12
- package/dist/{chunk-YSE2Z4RB.js.map → chunk-PQQFOONG.js.map} +1 -1
- package/dist/chunk-RW6XZGZG.mjs +156 -0
- package/dist/chunk-RW6XZGZG.mjs.map +1 -0
- package/dist/chunk-SYMI4IDW.js +90 -0
- package/dist/chunk-SYMI4IDW.js.map +1 -0
- package/dist/{chunk-N2BFT7KI.mjs → chunk-W7U6O3OE.mjs} +2 -71
- package/dist/chunk-W7U6O3OE.mjs.map +1 -0
- package/dist/{chunk-YMCL3JXZ.mjs → chunk-YPBIPE2B.mjs} +2 -2
- package/dist/{chunk-P7L6OFX5.js → chunk-Z6T2575A.js} +12 -81
- package/dist/chunk-Z6T2575A.js.map +1 -0
- package/dist/client/index.js +13 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +17 -14
- package/dist/components/forms/DatePickerPopover.d.ts.map +1 -1
- package/dist/components/forms/DateRangeSelector.d.ts.map +1 -1
- package/dist/components/forms/FormDate.d.ts.map +1 -1
- package/dist/components/forms/FormDateTime.d.ts.map +1 -1
- package/dist/components/forms/time-cycle.d.ts +14 -0
- package/dist/components/forms/time-cycle.d.ts.map +1 -0
- package/dist/components/index.js +11 -8
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -7
- package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
- package/dist/contexts/DateLocaleContext.d.ts +56 -0
- package/dist/contexts/DateLocaleContext.d.ts.map +1 -0
- package/dist/contexts/index.d.ts +1 -0
- package/dist/contexts/index.d.ts.map +1 -1
- package/dist/contexts/index.js +24 -8
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +23 -7
- package/dist/core/index.js +17 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +19 -8
- package/dist/date-locale/index.d.ts +2 -0
- package/dist/date-locale/index.d.ts.map +1 -0
- package/dist/date-locale/index.js +21 -0
- package/dist/date-locale/index.js.map +1 -0
- package/dist/date-locale/index.mjs +21 -0
- package/dist/email-automations/index.js +131 -123
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +23 -15
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/account-deletion/components/AccountDeletionAdminTrigger.d.ts.map +1 -1
- package/dist/features/analytics/components/AnalyticsJourneyContainer.d.ts.map +1 -1
- package/dist/features/analytics/components/AnalyticsSessionsTable.d.ts.map +1 -1
- package/dist/features/analytics/index.js +154 -147
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +32 -25
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +11 -9
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +9 -7
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/billing/components/utils/date.d.ts +3 -3
- package/dist/features/billing/components/utils/format-options.d.ts +6 -4
- package/dist/features/billing/components/utils/format-options.d.ts.map +1 -1
- package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts +5 -1
- package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts.map +1 -1
- package/dist/features/billing/stripe-usage/components/details/UsageSummaryCard.d.ts.map +1 -1
- package/dist/features/billing/stripe-usage/components/lists/UsageHistoryTable.d.ts.map +1 -1
- package/dist/features/blog/components/admin/BlogTranslationStatus.d.ts.map +1 -1
- package/dist/features/blog/components/public/BlogPostMeta.d.ts.map +1 -1
- package/dist/features/blog/hooks/useBlogPostTableStructure.d.ts.map +1 -1
- package/dist/features/blog/index.js +168 -162
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +25 -19
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +29 -27
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +8 -6
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.d.ts.map +1 -1
- package/dist/features/handbook/components/parts/HandbookIndexStatus.d.ts.map +1 -1
- package/dist/features/help/components/HelpArticleBody.d.ts.map +1 -1
- package/dist/features/help/index.js +48 -43
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +15 -10
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/notification/components/notifications/Notification.d.ts +2 -1
- package/dist/features/notification/components/notifications/Notification.d.ts.map +1 -1
- package/dist/features/notification/contexts/NotificationContext.d.ts.map +1 -1
- package/dist/features/notification/hooks/useNotificationTableStructure.d.ts.map +1 -1
- package/dist/features/oauth/components/OAuthClientCard.d.ts.map +1 -1
- package/dist/features/oauth/components/OAuthClientDetail.d.ts.map +1 -1
- package/dist/features/tokenusage/index.js +133 -130
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +10 -7
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/features/tokenusage/lib/formatters.d.ts +6 -3
- package/dist/features/tokenusage/lib/formatters.d.ts.map +1 -1
- package/dist/index.js +19 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -11
- package/dist/local-transport-AT4TE264.js +15 -0
- package/dist/{local-transport-MJPOKPTU.js.map → local-transport-AT4TE264.js.map} +1 -1
- package/dist/{local-transport-RVD4FZGA.mjs → local-transport-KWEUZ4GX.mjs} +5 -5
- package/dist/offline/index.js +56 -54
- package/dist/offline/index.js.map +1 -1
- package/dist/offline/index.mjs +23 -21
- package/dist/offline/index.mjs.map +1 -1
- package/dist/{request-4AWGPIGH.mjs → request-6GAIWWTG.mjs} +2 -2
- package/dist/{request-H5FZBZA2.js → request-BWO4Y434.js} +3 -3
- package/dist/{request-H5FZBZA2.js.map → request-BWO4Y434.js.map} +1 -1
- package/dist/server/ServerSession.d.ts +2 -0
- package/dist/server/ServerSession.d.ts.map +1 -1
- package/dist/server/index.js +10 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +9 -7
- package/dist/utils/date-formatter.d.ts +45 -4
- package/dist/utils/date-formatter.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/write-through-KNRI5L4V.mjs +15 -0
- package/dist/write-through-KNRI5L4V.mjs.map +1 -0
- package/dist/write-through-MYHQ6GJU.js +15 -0
- package/dist/{write-through-HE2QSOVI.js.map → write-through-MYHQ6GJU.js.map} +1 -1
- package/messages/en.json +1 -0
- package/messages/it.json +1 -0
- package/package.json +6 -1
- package/src/components/forms/DatePickerPopover.tsx +5 -3
- package/src/components/forms/DateRangeSelector.tsx +9 -7
- package/src/components/forms/FormDate.tsx +5 -3
- package/src/components/forms/FormDateTime.tsx +75 -39
- package/src/components/forms/__tests__/time-cycle.spec.ts +37 -0
- package/src/components/forms/time-cycle.ts +27 -0
- package/src/components/tables/cells/cell.date.tsx +3 -5
- package/src/components/tables/cells/cell.dateTime.tsx +3 -7
- package/src/contexts/DateLocaleContext.tsx +106 -0
- package/src/contexts/__tests__/DateLocaleContext.spec.tsx +175 -0
- package/src/contexts/index.ts +1 -0
- package/src/date-locale/index.ts +12 -0
- package/src/features/account-deletion/components/AccountDeletionAdminTrigger.tsx +3 -4
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +8 -8
- package/src/features/analytics/components/AnalyticsSessionsTable.tsx +4 -6
- package/src/features/auth/components/two-factor/PasskeyList.tsx +3 -3
- package/src/features/auth/components/two-factor/TotpAuthenticatorList.tsx +3 -3
- package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +14 -6
- package/src/features/billing/components/utils/date.ts +5 -5
- package/src/features/billing/components/utils/format-options.ts +6 -4
- package/src/features/billing/components/utils/useBillingFormatOptions.ts +8 -2
- package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +4 -12
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +4 -17
- package/src/features/blog/components/admin/BlogTranslationStatus.tsx +3 -2
- package/src/features/blog/components/public/BlogPostMeta.tsx +5 -4
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +1 -2
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +2 -3
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +4 -8
- package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +11 -5
- package/src/features/email-blast/components/containers/EmailBlastContainer.tsx +3 -3
- package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +4 -4
- package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +4 -3
- package/src/features/help/components/HelpArticleBody.tsx +4 -3
- package/src/features/notification/components/notifications/Notification.tsx +38 -7
- package/src/features/notification/contexts/NotificationContext.tsx +6 -3
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +6 -5
- package/src/features/oauth/components/OAuthClientCard.tsx +8 -5
- package/src/features/oauth/components/OAuthClientDetail.tsx +3 -6
- package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +19 -0
- package/src/features/tokenusage/lib/formatters.ts +11 -6
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +5 -5
- package/src/server/ServerSession.ts +3 -0
- package/src/server/__tests__/ServerSession.read.spec.ts +18 -1
- package/src/utils/__tests__/date-formatter.spec.ts +134 -10
- package/src/utils/date-formatter.ts +200 -44
- package/src/utils/index.ts +11 -1
- package/dist/BlockNoteEditor-MONT74RQ.js.map +0 -1
- package/dist/BlockNoteViewer-K2HBF3IP.js.map +0 -1
- package/dist/JsonApiRequest-KC6DOHKT.js +0 -26
- package/dist/chunk-7OIWOPM5.mjs.map +0 -1
- package/dist/chunk-BDWV4KYR.js.map +0 -1
- package/dist/chunk-HN7L6RRW.js.map +0 -1
- package/dist/chunk-N2BFT7KI.mjs.map +0 -1
- package/dist/chunk-OZADFYE7.mjs.map +0 -1
- package/dist/chunk-P7L6OFX5.js.map +0 -1
- package/dist/chunk-SO3YUCL6.js.map +0 -1
- package/dist/chunk-T6G2F2BK.mjs.map +0 -1
- package/dist/chunk-TNTCLVUR.js.map +0 -1
- package/dist/local-transport-MJPOKPTU.js +0 -15
- package/dist/write-through-7I3LA2WN.mjs +0 -15
- package/dist/write-through-HE2QSOVI.js +0 -15
- /package/dist/{JsonApiRequest-ADLBJ5RX.mjs.map → JsonApiRequest-WNPDS356.mjs.map} +0 -0
- /package/dist/{chunk-VNUT2D27.mjs.map → chunk-2PWYG2WJ.mjs.map} +0 -0
- /package/dist/{chunk-W4ML6D2X.mjs.map → chunk-3U7YJ73T.mjs.map} +0 -0
- /package/dist/{chunk-BE6UMM5X.mjs.map → chunk-47HNJQ7U.mjs.map} +0 -0
- /package/dist/{chunk-S7SPAC6X.mjs.map → chunk-73NKUBMP.mjs.map} +0 -0
- /package/dist/{chunk-YTDGYI62.mjs.map → chunk-DWPHHUMV.mjs.map} +0 -0
- /package/dist/{chunk-YMCL3JXZ.mjs.map → chunk-YPBIPE2B.mjs.map} +0 -0
- /package/dist/{local-transport-RVD4FZGA.mjs.map → date-locale/index.mjs.map} +0 -0
- /package/dist/{request-4AWGPIGH.mjs.map → local-transport-KWEUZ4GX.mjs.map} +0 -0
- /package/dist/{write-through-7I3LA2WN.mjs.map → request-6GAIWWTG.mjs.map} +0 -0
|
@@ -5,7 +5,7 @@ import { Send } from "lucide-react";
|
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
import { TableContent, UseTableStructureHook } from "../../../hooks";
|
|
8
|
-
import {
|
|
8
|
+
import { useDateFormatter } from "../../../contexts/DateLocaleContext";
|
|
9
9
|
import { Badge, Button } from "../../../shadcnui";
|
|
10
10
|
import { WaitlistFields } from "../data/waitlist.fields";
|
|
11
11
|
import { WaitlistInterface, WaitlistStatus } from "../data/WaitlistInterface";
|
|
@@ -19,7 +19,7 @@ import { formatSignupSource } from "./formatSignupSource";
|
|
|
19
19
|
*/
|
|
20
20
|
export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface, WaitlistFields> = (params) => {
|
|
21
21
|
const t = useTranslations();
|
|
22
|
-
const
|
|
22
|
+
const dates = useDateFormatter();
|
|
23
23
|
const onInvite: ((entry: WaitlistInterface) => void) | undefined = params.context?.onInvite;
|
|
24
24
|
|
|
25
25
|
const tableData = useMemo(() => {
|
|
@@ -71,7 +71,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
|
|
|
71
71
|
cell: ({ row }: { row: TableContent<WaitlistInterface> }) => {
|
|
72
72
|
const entry: WaitlistInterface = row.original.jsonApiData;
|
|
73
73
|
if (!entry.createdAt) return "-";
|
|
74
|
-
return
|
|
74
|
+
return dates.format(entry.createdAt, "date");
|
|
75
75
|
},
|
|
76
76
|
enableSorting: false,
|
|
77
77
|
enableHiding: false,
|
|
@@ -146,7 +146,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
|
|
|
146
146
|
if (entry.status === "invited" && entry.invitedAt) {
|
|
147
147
|
return (
|
|
148
148
|
<span className="text-muted-foreground text-xs">
|
|
149
|
-
{t("waitlist.admin.actions.invited_on", { date:
|
|
149
|
+
{t("waitlist.admin.actions.invited_on", { date: dates.format(entry.invitedAt, "dateMedium") })}
|
|
150
150
|
</span>
|
|
151
151
|
);
|
|
152
152
|
}
|
|
@@ -168,7 +168,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
|
|
|
168
168
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
169
169
|
TableContent<WaitlistInterface>
|
|
170
170
|
>[];
|
|
171
|
-
}, [params.fields, fieldColumnMap, t,
|
|
171
|
+
}, [params.fields, fieldColumnMap, t, dates]);
|
|
172
172
|
|
|
173
173
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
174
174
|
};
|
|
@@ -12,6 +12,8 @@ export interface SessionSnapshot {
|
|
|
12
12
|
token?: string;
|
|
13
13
|
sidebarOpen: boolean;
|
|
14
14
|
isMobile: boolean;
|
|
15
|
+
/** The request's raw `Accept-Language` header; the date locale is resolved from it on the client. */
|
|
16
|
+
acceptLanguage?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export class ServerSession {
|
|
@@ -29,6 +31,7 @@ export class ServerSession {
|
|
|
29
31
|
token,
|
|
30
32
|
sidebarOpen: cookieStore.get("sidebar_state")?.value !== "false",
|
|
31
33
|
isMobile: viewportCookie ? viewportCookie === "1" : userAgent({ headers: headerStore }).device.type === "mobile",
|
|
34
|
+
acceptLanguage: headerStore.get("accept-language") ?? undefined,
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -4,13 +4,14 @@ const mocks = vi.hoisted(() => {
|
|
|
4
4
|
const state = {
|
|
5
5
|
cookies: {} as Record<string, string>,
|
|
6
6
|
deviceType: undefined as string | undefined,
|
|
7
|
+
headers: {} as Record<string, string>,
|
|
7
8
|
};
|
|
8
9
|
return {
|
|
9
10
|
state,
|
|
10
11
|
cookies: vi.fn(async () => ({
|
|
11
12
|
get: (name: string) => (name in state.cookies ? { name, value: state.cookies[name] } : undefined),
|
|
12
13
|
})),
|
|
13
|
-
headers: vi.fn(async () => new Headers()),
|
|
14
|
+
headers: vi.fn(async () => new Headers(state.headers)),
|
|
14
15
|
userAgent: vi.fn(() => ({ device: { type: state.deviceType } })),
|
|
15
16
|
};
|
|
16
17
|
});
|
|
@@ -27,6 +28,7 @@ describe("ServerSession.read", () => {
|
|
|
27
28
|
configureRoles({ Administrator: "role-admin", CompanyAdministrator: "role-company-admin" });
|
|
28
29
|
mocks.state.cookies = {};
|
|
29
30
|
mocks.state.deviceType = undefined;
|
|
31
|
+
mocks.state.headers = {};
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
it("reads a logged-in administrator on a desktop with the sidebar closed", async () => {
|
|
@@ -64,4 +66,19 @@ describe("ServerSession.read", () => {
|
|
|
64
66
|
isMobile: false,
|
|
65
67
|
});
|
|
66
68
|
});
|
|
69
|
+
|
|
70
|
+
it("carries the Accept-Language header", async () => {
|
|
71
|
+
mocks.state.headers = { "accept-language": "en-GB,en;q=0.9" };
|
|
72
|
+
|
|
73
|
+
const snapshot = await ServerSession.read();
|
|
74
|
+
|
|
75
|
+
expect(snapshot.acceptLanguage).toBe("en-GB,en;q=0.9");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("leaves acceptLanguage undefined when the header is absent", async () => {
|
|
79
|
+
const snapshot = await ServerSession.read();
|
|
80
|
+
|
|
81
|
+
expect(snapshot).toHaveProperty("acceptLanguage", undefined);
|
|
82
|
+
expect(snapshot.acceptLanguage).toBeUndefined();
|
|
83
|
+
});
|
|
67
84
|
});
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DATE_STYLE_OPTIONS,
|
|
4
|
+
dateFormatLocale,
|
|
5
|
+
type DateFormatStyle,
|
|
6
|
+
formatDate,
|
|
7
|
+
formatDateStyle,
|
|
8
|
+
formatRelativeDate,
|
|
9
|
+
PUBLIC_STYLE_MAP,
|
|
10
|
+
resolveDateLocale,
|
|
11
|
+
usesTwelveHourClock,
|
|
12
|
+
} from "../date-formatter";
|
|
3
13
|
|
|
4
14
|
describe("formatDate", () => {
|
|
5
15
|
beforeEach(() => {
|
|
@@ -117,21 +127,21 @@ describe("formatDate", () => {
|
|
|
117
127
|
const date = new Date("2024-06-15T11:59:00Z");
|
|
118
128
|
const result = formatDate(date, "timeSince");
|
|
119
129
|
|
|
120
|
-
expect(result).toBe("1
|
|
130
|
+
expect(result).toBe("1 minute ago");
|
|
121
131
|
});
|
|
122
132
|
|
|
123
133
|
it("should handle 1 hour ago", () => {
|
|
124
134
|
const date = new Date("2024-06-15T11:00:00Z");
|
|
125
135
|
const result = formatDate(date, "timeSince");
|
|
126
136
|
|
|
127
|
-
expect(result).toBe("1
|
|
137
|
+
expect(result).toBe("1 hour ago");
|
|
128
138
|
});
|
|
129
139
|
|
|
130
140
|
it("should handle 1 day ago", () => {
|
|
131
141
|
const date = new Date("2024-06-14T12:00:00Z");
|
|
132
142
|
const result = formatDate(date, "timeSince");
|
|
133
143
|
|
|
134
|
-
expect(result).toBe("1
|
|
144
|
+
expect(result).toBe("1 day ago");
|
|
135
145
|
});
|
|
136
146
|
|
|
137
147
|
it("should throw for invalid format option", () => {
|
|
@@ -171,14 +181,128 @@ describe("formatDate", () => {
|
|
|
171
181
|
});
|
|
172
182
|
|
|
173
183
|
describe("dateFormatLocale", () => {
|
|
174
|
-
it("
|
|
175
|
-
expect(dateFormatLocale(
|
|
176
|
-
expect(dateFormatLocale("en
|
|
177
|
-
expect(dateFormatLocale(
|
|
184
|
+
it("returns the app locale, English when missing", () => {
|
|
185
|
+
expect(dateFormatLocale(undefined)).toBe("en");
|
|
186
|
+
expect(dateFormatLocale("en")).toBe("en");
|
|
187
|
+
expect(dateFormatLocale("it")).toBe("it");
|
|
178
188
|
});
|
|
189
|
+
});
|
|
179
190
|
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
describe("explicit now", () => {
|
|
192
|
+
it("measures timeSince against the passed now", () => {
|
|
193
|
+
const now = new Date("2030-01-02T00:00:00Z");
|
|
194
|
+
expect(formatDate(new Date("2030-01-01T00:00:00Z"), "timeSince", "en-GB", now)).toBe("1 day ago");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const normalise = (value: string): string => value.replace(/[\u202f\u00a0]/g, " ");
|
|
200
|
+
const sample = new Date("2026-04-25T21:30:00Z");
|
|
201
|
+
|
|
202
|
+
describe("resolveDateLocale", () => {
|
|
203
|
+
it("takes the first browser locale whose language matches the app language", () => {
|
|
204
|
+
expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "en-GB,en;q=0.9" })).toBe("en-GB");
|
|
205
|
+
expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "en-US,en;q=0.9" })).toBe("en-US");
|
|
206
|
+
expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "en" })).toBe("en");
|
|
207
|
+
expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "it-IT,it;q=0.9,en-GB;q=0.8" })).toBe("en-GB");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("falls back to the app language when no browser locale matches", () => {
|
|
211
|
+
expect(resolveDateLocale({ appLocale: "it", acceptLanguage: "en-GB" })).toBe("it");
|
|
212
|
+
expect(resolveDateLocale({ appLocale: "en" })).toBe("en");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("follows quality order, not header order", () => {
|
|
216
|
+
expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "fr-FR,en-US;q=0.5,en-GB;q=0.9" })).toBe("en-GB");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("resolveDateLocale tolerates malformed headers", () => {
|
|
220
|
+
for (const acceptLanguage of ["*", "en-GB;q=abc", "zz-ZZ", "en-GB;q=0", ",".repeat(2000), ""]) {
|
|
221
|
+
expect(() => resolveDateLocale({ appLocale: "en", acceptLanguage })).not.toThrow();
|
|
222
|
+
expect(resolveDateLocale({ appLocale: "en", acceptLanguage })).toBe("en");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("formatDateStyle", () => {
|
|
228
|
+
const expected: Record<DateFormatStyle, { "en-GB": string; "en-US": string }> = {
|
|
229
|
+
date: { "en-GB": "25/04/2026", "en-US": "04/25/2026" },
|
|
230
|
+
dateMedium: { "en-GB": "25 Apr 2026", "en-US": "Apr 25, 2026" },
|
|
231
|
+
dateLong: { "en-GB": "25 April 2026", "en-US": "April 25, 2026" },
|
|
232
|
+
dateTime: { "en-GB": "25/04/2026, 21:30", "en-US": "04/25/2026, 9:30 PM" },
|
|
233
|
+
dateTimeMedium: { "en-GB": "25 Apr 2026, 21:30", "en-US": "Apr 25, 2026, 9:30 PM" },
|
|
234
|
+
dateTimeLong: { "en-GB": "25 April 2026 at 21:30", "en-US": "April 25, 2026 at 9:30 PM" },
|
|
235
|
+
dateTimeSeconds: { "en-GB": "25/04/2026, 21:30:00", "en-US": "04/25/2026, 9:30:00 PM" },
|
|
236
|
+
time: { "en-GB": "21:30", "en-US": "9:30 PM" },
|
|
237
|
+
weekdayDateTime: { "en-GB": "Sat 25 Apr, 21:30", "en-US": "Sat, Apr 25, 9:30 PM" },
|
|
238
|
+
weekdayDay: { "en-GB": "Sat, 25 April 2026", "en-US": "Sat, April 25, 2026" },
|
|
239
|
+
monthYear: { "en-GB": "Apr 2026", "en-US": "Apr 2026" },
|
|
240
|
+
monthYearLong: { "en-GB": "April 2026", "en-US": "April 2026" },
|
|
241
|
+
month: { "en-GB": "Apr", "en-US": "Apr" },
|
|
242
|
+
monthLong: { "en-GB": "April", "en-US": "April" },
|
|
243
|
+
dayMonth: { "en-GB": "25 Apr", "en-US": "Apr 25" },
|
|
244
|
+
weekday: { "en-GB": "Sat", "en-US": "Sat" },
|
|
245
|
+
weekdayLong: { "en-GB": "Saturday", "en-US": "Saturday" },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
for (const style of Object.keys(expected) as DateFormatStyle[]) {
|
|
249
|
+
for (const locale of ["en-GB", "en-US"] as const) {
|
|
250
|
+
it(`formats ${style} for ${locale}`, () => {
|
|
251
|
+
expect(normalise(formatDateStyle({ date: sample, style, locale, timeZone: "UTC" }))).toBe(
|
|
252
|
+
expected[style][locale],
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
it("never pins the clock", () => {
|
|
259
|
+
for (const options of Object.values(DATE_STYLE_OPTIONS)) {
|
|
260
|
+
expect(options).not.toHaveProperty("hour12");
|
|
261
|
+
expect(options).not.toHaveProperty("hourCycle");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("maps numeric styles to month-as-word in public mode", () => {
|
|
266
|
+
expect(PUBLIC_STYLE_MAP).toEqual({
|
|
267
|
+
date: "dateMedium",
|
|
268
|
+
dateTime: "dateTimeMedium",
|
|
269
|
+
dateTimeSeconds: "dateTimeMedium",
|
|
182
270
|
});
|
|
271
|
+
expect(formatDateStyle({ date: sample, style: "date", locale: "en-GB", timeZone: "UTC", isPublic: true })).toBe(
|
|
272
|
+
"25 Apr 2026",
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("accepts an ISO string", () => {
|
|
277
|
+
expect(formatDateStyle({ date: "2026-04-25T21:30:00Z", style: "date", locale: "en-GB", timeZone: "UTC" })).toBe(
|
|
278
|
+
"25/04/2026",
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("formatDateStyle returns empty for invalid dates", () => {
|
|
283
|
+
expect(formatDateStyle({ date: undefined, style: "date", locale: "en-GB" })).toBe("");
|
|
284
|
+
expect(formatDateStyle({ date: "nope", style: "date", locale: "en-GB" })).toBe("");
|
|
285
|
+
expect(formatDateStyle({ date: new Date(NaN), style: "date", locale: "en-GB" })).toBe("");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("formatRelativeDate", () => {
|
|
290
|
+
const now = new Date("2026-04-25T21:30:00Z");
|
|
291
|
+
|
|
292
|
+
it("says 1 day ago in English", () => {
|
|
293
|
+
expect(formatRelativeDate({ date: new Date("2026-04-24T21:30:00Z"), locale: "en", now })).toBe("1 day ago");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("uses Italian wording", () => {
|
|
297
|
+
expect(formatRelativeDate({ date: new Date("2026-04-25T18:30:00Z"), locale: "it", now })).toBe("3 ore fa");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("usesTwelveHourClock", () => {
|
|
302
|
+
it("is true for en-US and en, false for en-GB and it", () => {
|
|
303
|
+
expect(usesTwelveHourClock("en-US")).toBe(true);
|
|
304
|
+
expect(usesTwelveHourClock("en")).toBe(true);
|
|
305
|
+
expect(usesTwelveHourClock("en-GB")).toBe(false);
|
|
306
|
+
expect(usesTwelveHourClock("it")).toBe(false);
|
|
183
307
|
});
|
|
184
308
|
});
|
|
@@ -18,65 +18,221 @@ export const formatLocalDate = (d: Date): string => {
|
|
|
18
18
|
return `${year}-${month}-${day}`;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export type DateFormatStyle =
|
|
22
|
+
| "date"
|
|
23
|
+
| "dateMedium"
|
|
24
|
+
| "dateLong"
|
|
25
|
+
| "dateTime"
|
|
26
|
+
| "dateTimeMedium"
|
|
27
|
+
| "dateTimeLong"
|
|
28
|
+
| "dateTimeSeconds"
|
|
29
|
+
| "time"
|
|
30
|
+
| "weekdayDateTime"
|
|
31
|
+
| "weekdayDay"
|
|
32
|
+
| "monthYear"
|
|
33
|
+
| "monthYearLong"
|
|
34
|
+
| "month"
|
|
35
|
+
| "monthLong"
|
|
36
|
+
| "dayMonth"
|
|
37
|
+
| "weekday"
|
|
38
|
+
| "weekdayLong";
|
|
39
|
+
|
|
40
|
+
const NUMERIC_DATE: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit", year: "numeric" };
|
|
41
|
+
|
|
21
42
|
/**
|
|
22
|
-
* The
|
|
23
|
-
* the
|
|
24
|
-
* other app locale ("it") is used as is.
|
|
43
|
+
* The option set behind each named style. No style pins `hour12` / `hourCycle`:
|
|
44
|
+
* the clock (12 or 24 hours) follows the locale.
|
|
25
45
|
*/
|
|
26
|
-
export const
|
|
27
|
-
|
|
46
|
+
export const DATE_STYLE_OPTIONS: Record<DateFormatStyle, Intl.DateTimeFormatOptions> = {
|
|
47
|
+
date: NUMERIC_DATE,
|
|
48
|
+
dateMedium: { dateStyle: "medium" },
|
|
49
|
+
dateLong: { dateStyle: "long" },
|
|
50
|
+
dateTime: { ...NUMERIC_DATE, hour: "numeric", minute: "2-digit" },
|
|
51
|
+
dateTimeMedium: { dateStyle: "medium", timeStyle: "short" },
|
|
52
|
+
dateTimeLong: { day: "numeric", month: "long", year: "numeric", hour: "numeric", minute: "2-digit" },
|
|
53
|
+
dateTimeSeconds: { ...NUMERIC_DATE, hour: "numeric", minute: "2-digit", second: "2-digit" },
|
|
54
|
+
time: { hour: "numeric", minute: "2-digit" },
|
|
55
|
+
weekdayDateTime: { weekday: "short", day: "numeric", month: "short", hour: "numeric", minute: "2-digit" },
|
|
56
|
+
weekdayDay: { weekday: "short", day: "numeric", month: "long", year: "numeric" },
|
|
57
|
+
monthYear: { month: "short", year: "numeric" },
|
|
58
|
+
monthYearLong: { month: "long", year: "numeric" },
|
|
59
|
+
month: { month: "short" },
|
|
60
|
+
monthLong: { month: "long" },
|
|
61
|
+
dayMonth: { day: "numeric", month: "short" },
|
|
62
|
+
weekday: { weekday: "short" },
|
|
63
|
+
weekdayLong: { weekday: "long" },
|
|
64
|
+
};
|
|
28
65
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
/** Public pages never show an all-numeric date: the month is spelled out. */
|
|
67
|
+
export const PUBLIC_STYLE_MAP: Partial<Record<DateFormatStyle, DateFormatStyle>> = {
|
|
68
|
+
date: "dateMedium",
|
|
69
|
+
dateTime: "dateTimeMedium",
|
|
70
|
+
dateTimeSeconds: "dateTimeMedium",
|
|
71
|
+
};
|
|
32
72
|
|
|
33
|
-
|
|
34
|
-
const diff = now.getTime() - eventDate.getTime();
|
|
73
|
+
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
|
35
74
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
/** One cached `Intl.DateTimeFormat` per locale + option-set key + time zone. */
|
|
76
|
+
const cachedDateTimeFormat = (
|
|
77
|
+
locale: string,
|
|
78
|
+
key: string,
|
|
79
|
+
options: Intl.DateTimeFormatOptions,
|
|
80
|
+
timeZone?: string,
|
|
81
|
+
): Intl.DateTimeFormat => {
|
|
82
|
+
const cacheKey = `${locale}|${key}|${timeZone ?? ""}`;
|
|
83
|
+
let formatter = formatterCache.get(cacheKey);
|
|
84
|
+
if (!formatter) {
|
|
85
|
+
formatter = new Intl.DateTimeFormat(locale, timeZone ? { ...options, timeZone } : options);
|
|
86
|
+
formatterCache.set(cacheKey, formatter);
|
|
87
|
+
}
|
|
88
|
+
return formatter;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const relativeCache = new Map<string, Intl.RelativeTimeFormat>();
|
|
92
|
+
|
|
93
|
+
const cachedRelativeTimeFormat = (locale: string): Intl.RelativeTimeFormat => {
|
|
94
|
+
let formatter = relativeCache.get(locale);
|
|
95
|
+
if (!formatter) {
|
|
96
|
+
formatter = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
|
|
97
|
+
relativeCache.set(locale, formatter);
|
|
39
98
|
}
|
|
99
|
+
return formatter;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const toValidDate = (date: Date | string | undefined): Date | undefined => {
|
|
103
|
+
if (date === undefined || date === null) return undefined;
|
|
104
|
+
const value = date instanceof Date ? date : new Date(date);
|
|
105
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const languageOf = (tag: string): string => tag.split("-")[0].toLowerCase();
|
|
109
|
+
|
|
110
|
+
const MAX_ACCEPT_LANGUAGE_ENTRIES = 50;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The locale every real-world date is formatted in.
|
|
114
|
+
*
|
|
115
|
+
* The first `Accept-Language` entry (quality order) whose language equals the
|
|
116
|
+
* app language's and that `Intl` supports; otherwise the app language. A
|
|
117
|
+
* malformed entry (`*`, an unparseable or zero quality, an unknown tag) is
|
|
118
|
+
* skipped, never thrown on.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveDateLocale(params: { appLocale: string; acceptLanguage?: string }): string {
|
|
121
|
+
const { appLocale, acceptLanguage } = params;
|
|
122
|
+
if (!acceptLanguage) return appLocale;
|
|
123
|
+
|
|
124
|
+
const appLanguage = languageOf(appLocale);
|
|
125
|
+
const entries: { tag: string; quality: number; index: number }[] = [];
|
|
126
|
+
|
|
127
|
+
const parts = acceptLanguage.split(",");
|
|
128
|
+
for (let index = 0; index < parts.length && entries.length < MAX_ACCEPT_LANGUAGE_ENTRIES; index++) {
|
|
129
|
+
const [rawTag, ...rawParams] = parts[index].split(";");
|
|
130
|
+
const tag = rawTag.trim();
|
|
131
|
+
if (!tag || tag === "*") continue;
|
|
40
132
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (locale.split("-")[0] !== "en") {
|
|
48
|
-
const relative = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
|
|
49
|
-
if (days > 0) return relative.format(-days, "day");
|
|
50
|
-
if (hours > 0) return relative.format(-hours, "hour");
|
|
51
|
-
if (minutes > 0) return relative.format(-minutes, "minute");
|
|
52
|
-
return relative.format(-seconds, "second");
|
|
133
|
+
let quality = 1;
|
|
134
|
+
const qualityParam = rawParams.map((param) => param.trim()).find((param) => param.startsWith("q="));
|
|
135
|
+
if (qualityParam !== undefined) {
|
|
136
|
+
const raw = qualityParam.slice(2);
|
|
137
|
+
quality = /^\d+(\.\d+)?$/.test(raw) ? Number(raw) : NaN;
|
|
53
138
|
}
|
|
139
|
+
if (!(quality > 0)) continue;
|
|
140
|
+
if (languageOf(tag) !== appLanguage) continue;
|
|
54
141
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
142
|
+
entries.push({ tag, quality, index });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
entries.sort((a, b) => b.quality - a.quality || a.index - b.index);
|
|
146
|
+
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
try {
|
|
149
|
+
const [supported] = Intl.DateTimeFormat.supportedLocalesOf([entry.tag]);
|
|
150
|
+
if (supported && languageOf(supported) === appLanguage) return supported;
|
|
151
|
+
} catch {
|
|
152
|
+
// An invalid language tag: skip it.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return appLocale;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Formats a date in a named style. Returns "" for `undefined` or a value that
|
|
161
|
+
* does not parse. `isPublic` swaps the all-numeric styles for month-as-word.
|
|
162
|
+
*/
|
|
163
|
+
export function formatDateStyle(params: {
|
|
164
|
+
date: Date | string | undefined;
|
|
165
|
+
style: DateFormatStyle;
|
|
166
|
+
locale: string;
|
|
167
|
+
timeZone?: string;
|
|
168
|
+
isPublic?: boolean;
|
|
169
|
+
}): string {
|
|
170
|
+
const value = toValidDate(params.date);
|
|
171
|
+
if (!value) return "";
|
|
172
|
+
const style = (params.isPublic && PUBLIC_STYLE_MAP[params.style]) || params.style;
|
|
173
|
+
return cachedDateTimeFormat(params.locale, style, DATE_STYLE_OPTIONS[style], params.timeZone).format(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** "3 hours ago" / "3 ore fa", always numeric ("1 day ago", never "yesterday"). */
|
|
177
|
+
export function formatRelativeDate(params: { date: Date | string; locale: string; now?: Date }): string {
|
|
178
|
+
const value = toValidDate(params.date);
|
|
179
|
+
if (!value) return "";
|
|
180
|
+
const now = params.now ?? new Date();
|
|
181
|
+
const diff = now.getTime() - value.getTime();
|
|
182
|
+
const sign = diff < 0 ? 1 : -1;
|
|
183
|
+
const seconds = Math.floor(Math.abs(diff) / 1000);
|
|
184
|
+
const minutes = Math.floor(seconds / 60);
|
|
185
|
+
const hours = Math.floor(minutes / 60);
|
|
186
|
+
const days = Math.floor(hours / 24);
|
|
187
|
+
|
|
188
|
+
const relative = cachedRelativeTimeFormat(params.locale);
|
|
189
|
+
if (days > 0) return relative.format(sign * days, "day");
|
|
190
|
+
if (hours > 0) return relative.format(sign * hours, "hour");
|
|
191
|
+
if (minutes > 0) return relative.format(sign * minutes, "minute");
|
|
192
|
+
return relative.format(sign * seconds, "second");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** True when the locale's clock is 12-hour (hour cycle h11 / h12). */
|
|
196
|
+
export function usesTwelveHourClock(locale: string): boolean {
|
|
197
|
+
const hourCycle = cachedDateTimeFormat(locale, "__hourCycle", { hour: "numeric" }).resolvedOptions().hourCycle;
|
|
198
|
+
return hourCycle === "h11" || hourCycle === "h12";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The locale to format dates in for an app locale: the app locale itself, English when missing. */
|
|
202
|
+
export const dateFormatLocale = (appLocale?: string): string => appLocale ?? "en";
|
|
203
|
+
|
|
204
|
+
const LEGACY_DATE_OPTIONS: Intl.DateTimeFormatOptions = { year: "numeric", month: "2-digit", day: "2-digit" };
|
|
205
|
+
const LEGACY_TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" };
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @deprecated Use `formatDateStyle` (or `useDateFormatter().format`) and
|
|
209
|
+
* `formatRelativeDate` (or `useDateFormatter().relative`). Kept for other nja
|
|
210
|
+
* apps: same default locale and the same "date" / "time" / "dateTime" output.
|
|
211
|
+
* Pass `now` from render code; the `new Date()` fallback is for legacy callers.
|
|
212
|
+
*/
|
|
213
|
+
export const formatDate = (
|
|
214
|
+
eventDate: Date,
|
|
215
|
+
formatOption: FormatOption,
|
|
216
|
+
locale: string = "en-GB",
|
|
217
|
+
now?: Date,
|
|
218
|
+
): string => {
|
|
219
|
+
const reference = now ?? new Date();
|
|
220
|
+
const diff = reference.getTime() - eventDate.getTime();
|
|
221
|
+
|
|
222
|
+
if (formatOption === "default") {
|
|
223
|
+
if (diff < 24 * 60 * 60 * 1000) formatOption = "timeSince";
|
|
224
|
+
else formatOption = "dateTime";
|
|
59
225
|
}
|
|
60
226
|
|
|
61
|
-
// Define formatting options
|
|
62
|
-
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
63
|
-
year: "numeric",
|
|
64
|
-
month: "2-digit",
|
|
65
|
-
day: "2-digit",
|
|
66
|
-
};
|
|
67
|
-
const timeOptions: Intl.DateTimeFormatOptions = {
|
|
68
|
-
hour: "2-digit",
|
|
69
|
-
minute: "2-digit",
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Format based on the option
|
|
73
227
|
switch (formatOption) {
|
|
228
|
+
case "timeSince":
|
|
229
|
+
return formatRelativeDate({ date: eventDate, locale, now: reference });
|
|
74
230
|
case "date":
|
|
75
|
-
return
|
|
231
|
+
return cachedDateTimeFormat(locale, "__legacyDate", LEGACY_DATE_OPTIONS).format(eventDate);
|
|
76
232
|
case "time":
|
|
77
|
-
return
|
|
233
|
+
return cachedDateTimeFormat(locale, "__legacyTime", LEGACY_TIME_OPTIONS).format(eventDate);
|
|
78
234
|
case "dateTime":
|
|
79
|
-
return `${
|
|
235
|
+
return `${cachedDateTimeFormat(locale, "__legacyDate", LEGACY_DATE_OPTIONS).format(eventDate)} ${cachedDateTimeFormat(locale, "__legacyTime", LEGACY_TIME_OPTIONS).format(eventDate)}`;
|
|
80
236
|
default:
|
|
81
237
|
throw new Error("Invalid format option");
|
|
82
238
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -4,7 +4,17 @@ export { composeRefs, useComposedRefs } from "./compose-refs";
|
|
|
4
4
|
export { useIsMobile, MOBILE_BREAKPOINT, VIEWPORT_COOKIE_NAME } from "./use-mobile";
|
|
5
5
|
|
|
6
6
|
// New utilities
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
dateFormatLocale,
|
|
9
|
+
formatDate,
|
|
10
|
+
formatDateStyle,
|
|
11
|
+
formatLocalDate,
|
|
12
|
+
formatRelativeDate,
|
|
13
|
+
resolveDateLocale,
|
|
14
|
+
usesTwelveHourClock,
|
|
15
|
+
type DateFormatStyle,
|
|
16
|
+
type FormatOption,
|
|
17
|
+
} from "./date-formatter";
|
|
8
18
|
export { exists } from "./exists";
|
|
9
19
|
export { getTableComponents, getTableOptions, TableOptions } from "./table-options";
|
|
10
20
|
|