@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
|
@@ -4,6 +4,7 @@ import { LoaderCircleIcon, RotateCcwIcon } from "lucide-react";
|
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
5
|
import { useCallback, useMemo, useState } from "react";
|
|
6
6
|
import { errorToast } from "../../../../components";
|
|
7
|
+
import { useDateFormatter } from "../../../../contexts/DateLocaleContext";
|
|
7
8
|
import { useI18nLocale } from "../../../../i18n";
|
|
8
9
|
import { Badge, Button } from "../../../../shadcnui";
|
|
9
10
|
import { BlogCategoryInterface } from "../../data";
|
|
@@ -47,8 +48,8 @@ export function useBlogLanguageName(): (language: string) => string {
|
|
|
47
48
|
|
|
48
49
|
/** Formats a date for the admin pages in the viewer's locale. */
|
|
49
50
|
export function useBlogDateFormatter(): (date?: Date) => string {
|
|
50
|
-
const
|
|
51
|
-
return useCallback((date?: Date) => (date
|
|
51
|
+
const dates = useDateFormatter();
|
|
52
|
+
return useCallback((date?: Date) => dates.format(date, "dateMedium"), [dates]);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
type BlogTranslationStatusProps = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useDateFormatter } from "../../../../contexts/DateLocaleContext";
|
|
4
5
|
import { cn } from "../../../../utils/cn";
|
|
5
6
|
import type { PublicBlogPost } from "../../types/blog-public.types";
|
|
6
7
|
|
|
@@ -19,12 +20,12 @@ export function BlogPostMeta(props: {
|
|
|
19
20
|
}) {
|
|
20
21
|
const { post, showAuthor, showDate, showUpdated, className } = props;
|
|
21
22
|
const t = useTranslations();
|
|
22
|
-
const
|
|
23
|
+
const dates = useDateFormatter();
|
|
23
24
|
|
|
24
25
|
const parts: string[] = [];
|
|
25
26
|
if (showAuthor && post.authorName) parts.push(t("blog.public.by", { author: post.authorName }));
|
|
26
27
|
if (showDate && post.publishedAt) {
|
|
27
|
-
parts.push(format
|
|
28
|
+
parts.push(dates.format(post.publishedAt, "dateMedium"));
|
|
28
29
|
}
|
|
29
30
|
if (
|
|
30
31
|
showUpdated &&
|
|
@@ -32,7 +33,7 @@ export function BlogPostMeta(props: {
|
|
|
32
33
|
post.updatedAt &&
|
|
33
34
|
new Date(post.updatedAt).getTime() - new Date(post.publishedAt).getTime() > ONE_DAY_MS
|
|
34
35
|
) {
|
|
35
|
-
parts.push(t("blog.public.updated", { date: format
|
|
36
|
+
parts.push(t("blog.public.updated", { date: dates.format(post.updatedAt, "dateMedium") }));
|
|
36
37
|
}
|
|
37
38
|
parts.push(t("blog.public.readingTime", { minutes: post.readingMinutes }));
|
|
38
39
|
|
|
@@ -9,7 +9,6 @@ import { BlogList } from "../BlogList";
|
|
|
9
9
|
vi.mock("next-intl", () => ({
|
|
10
10
|
useTranslations: () => (key: string, values?: Record<string, unknown>) =>
|
|
11
11
|
values ? `${key} ${Object.values(values).join(" ")}` : key,
|
|
12
|
-
useFormatter: () => ({ dateTime: (date: Date) => date.toISOString().slice(0, 10) }),
|
|
13
12
|
}));
|
|
14
13
|
vi.mock("next/image", () => ({
|
|
15
14
|
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
|
@@ -101,7 +100,7 @@ describe("BlogList", () => {
|
|
|
101
100
|
expect(screen.getByRole("heading", { level: 1, name: "blog.public.title" })).toBeInTheDocument();
|
|
102
101
|
expect(screen.getByText("blog.public.eyebrow")).toBeInTheDocument();
|
|
103
102
|
// The public blog shows no dates in any layout.
|
|
104
|
-
expect(screen.queryByText(/2026
|
|
103
|
+
expect(screen.queryByText(/Oct 12, 2026|12 Oct 2026/)).not.toBeInTheDocument();
|
|
105
104
|
});
|
|
106
105
|
|
|
107
106
|
it("active category chip is marked and links back to all", () => {
|
|
@@ -9,7 +9,6 @@ import { BlogPostView } from "../BlogPostView";
|
|
|
9
9
|
vi.mock("next-intl", () => ({
|
|
10
10
|
useTranslations: () => (key: string, values?: Record<string, unknown>) =>
|
|
11
11
|
values ? `${key} ${Object.values(values).join(" ")}` : key,
|
|
12
|
-
useFormatter: () => ({ dateTime: (date: Date) => date.toISOString().slice(0, 10) }),
|
|
13
12
|
}));
|
|
14
13
|
vi.mock("next/image", () => ({
|
|
15
14
|
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
|
@@ -102,8 +101,8 @@ describe("BlogPostView", () => {
|
|
|
102
101
|
|
|
103
102
|
it("shows neither the publish date nor the updated date", () => {
|
|
104
103
|
render(<BlogPostView post={main} categories={cats} related={[]} basePath="/blog" />);
|
|
105
|
-
expect(screen.queryByText(/2026
|
|
106
|
-
expect(screen.queryByText(/2026
|
|
104
|
+
expect(screen.queryByText(/Oct 12, 2026|12 Oct 2026/)).not.toBeInTheDocument();
|
|
105
|
+
expect(screen.queryByText(/Oct 15, 2026|15 Oct 2026/)).not.toBeInTheDocument();
|
|
107
106
|
expect(screen.queryByText(/blog\.public\.updated/)).not.toBeInTheDocument();
|
|
108
107
|
});
|
|
109
108
|
|
|
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
|
|
|
5
5
|
import { useMemo } from "react";
|
|
6
6
|
import { Modules } from "../../../core";
|
|
7
7
|
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../hooks";
|
|
8
|
-
import {
|
|
8
|
+
import { useDateFormatter } from "../../../contexts/DateLocaleContext";
|
|
9
9
|
import { Badge, Link } from "../../../shadcnui";
|
|
10
10
|
import { BLOG_STATUS_BADGE } from "../components/admin/BlogTranslationStatus";
|
|
11
11
|
import { useBlogCategoryNames } from "./useBlogCategoryNames";
|
|
@@ -19,7 +19,7 @@ import { BlogPostFields, BlogPostInterface } from "../data";
|
|
|
19
19
|
*/
|
|
20
20
|
export const useBlogPostTableStructure: UseTableStructureHook<BlogPostInterface, BlogPostFields> = (params) => {
|
|
21
21
|
const t = useTranslations();
|
|
22
|
-
const
|
|
22
|
+
const dates = useDateFormatter();
|
|
23
23
|
const generateUrl = usePageUrlGenerator();
|
|
24
24
|
const categoryName = useBlogCategoryNames();
|
|
25
25
|
|
|
@@ -85,11 +85,7 @@ export const useBlogPostTableStructure: UseTableStructureHook<BlogPostInterface,
|
|
|
85
85
|
cell: ({ row }: { row: TableContent<BlogPostInterface> }) => {
|
|
86
86
|
const post: BlogPostInterface = row.original.jsonApiData;
|
|
87
87
|
if (!post.publishedAt) return <span className="text-muted-foreground text-xs">—</span>;
|
|
88
|
-
return (
|
|
89
|
-
<span className="text-xs tabular-nums">
|
|
90
|
-
{post.publishedAt.toLocaleDateString(locale, { dateStyle: "medium" })}
|
|
91
|
-
</span>
|
|
92
|
-
);
|
|
88
|
+
return <span className="text-xs tabular-nums">{dates.format(post.publishedAt, "dateMedium")}</span>;
|
|
93
89
|
},
|
|
94
90
|
enableSorting: false,
|
|
95
91
|
enableHiding: false,
|
|
@@ -118,7 +114,7 @@ export const useBlogPostTableStructure: UseTableStructureHook<BlogPostInterface,
|
|
|
118
114
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
119
115
|
TableContent<BlogPostInterface>
|
|
120
116
|
>[];
|
|
121
|
-
}, [params.fields, fieldColumnMap, t, generateUrl,
|
|
117
|
+
}, [params.fields, fieldColumnMap, t, generateUrl, dates, categoryName]);
|
|
122
118
|
|
|
123
119
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
124
120
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ColumnDef } from "@tanstack/react-table";
|
|
4
|
-
import {
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
5
|
import { useMemo } from "react";
|
|
6
6
|
import { Badge } from "../../../shadcnui";
|
|
7
7
|
import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
|
|
8
|
-
import {
|
|
8
|
+
import { useDateFormatter, useMountedNow } from "../../../contexts/DateLocaleContext";
|
|
9
|
+
import { formatDate } from "../../../utils/date-formatter";
|
|
9
10
|
import { EmailAutomationEnrollmentFields } from "../data/email-automation-enrollment.fields";
|
|
10
11
|
import { EmailAutomationEnrollmentInterface } from "../data/EmailAutomationEnrollmentInterface";
|
|
11
12
|
|
|
@@ -22,7 +23,8 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
|
|
|
22
23
|
EmailAutomationEnrollmentFields
|
|
23
24
|
> = (params) => {
|
|
24
25
|
const t = useTranslations();
|
|
25
|
-
const locale =
|
|
26
|
+
const { locale } = useDateFormatter();
|
|
27
|
+
const now = useMountedNow();
|
|
26
28
|
|
|
27
29
|
const tableData = useMemo(() => {
|
|
28
30
|
return params.data.map((enrollment: EmailAutomationEnrollmentInterface) => {
|
|
@@ -95,7 +97,11 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
|
|
|
95
97
|
cell: ({ row }: { row: TableContent<EmailAutomationEnrollmentInterface> }) => {
|
|
96
98
|
const enrollment: EmailAutomationEnrollmentInterface = row.original.jsonApiData;
|
|
97
99
|
if (!enrollment.nextSendAt) return empty;
|
|
98
|
-
return
|
|
100
|
+
return (
|
|
101
|
+
<span className="text-xs tabular-nums">
|
|
102
|
+
{now ? formatDate(enrollment.nextSendAt, "dateTime", locale, now) : ""}
|
|
103
|
+
</span>
|
|
104
|
+
);
|
|
99
105
|
},
|
|
100
106
|
enableSorting: false,
|
|
101
107
|
enableHiding: false,
|
|
@@ -118,7 +124,7 @@ export const useEmailAutomationEnrollmentTableStructure: UseTableStructureHook<
|
|
|
118
124
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
119
125
|
TableContent<EmailAutomationEnrollmentInterface>
|
|
120
126
|
>[];
|
|
121
|
-
}, [params.fields, fieldColumnMap, t, locale]);
|
|
127
|
+
}, [params.fields, fieldColumnMap, t, locale, now]);
|
|
122
128
|
|
|
123
129
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
124
130
|
};
|
|
@@ -6,10 +6,10 @@ import { ReactNode, useCallback, useEffect, useState } from "react";
|
|
|
6
6
|
import { CommonDeleter, RoundPageContainer } from "../../../../components";
|
|
7
7
|
import { errorToast } from "../../../../components/errors/errorToast";
|
|
8
8
|
import { DetailField } from "../../../../components/typography";
|
|
9
|
+
import { useDateFormatter } from "../../../../contexts/DateLocaleContext";
|
|
9
10
|
import { SharedProvider } from "../../../../contexts/SharedContext";
|
|
10
11
|
import { Modules } from "../../../../core";
|
|
11
12
|
import { usePageUrlGenerator } from "../../../../hooks";
|
|
12
|
-
import { useI18nLocale } from "../../../../i18n";
|
|
13
13
|
import { BreadcrumbItemData } from "../../../../interfaces";
|
|
14
14
|
import { Badge, Button } from "../../../../shadcnui";
|
|
15
15
|
import { showToast } from "../../../../utils/toast";
|
|
@@ -54,7 +54,7 @@ function EmailBlastSendTestButton({ blast }: { blast: EmailBlastInterface }) {
|
|
|
54
54
|
|
|
55
55
|
export function EmailBlastContainer({ blastId }: EmailBlastContainerProps) {
|
|
56
56
|
const t = useTranslations();
|
|
57
|
-
const
|
|
57
|
+
const dates = useDateFormatter();
|
|
58
58
|
const generateUrl = usePageUrlGenerator();
|
|
59
59
|
const [blast, setBlast] = useState<EmailBlastInterface | undefined>(undefined);
|
|
60
60
|
|
|
@@ -130,7 +130,7 @@ export function EmailBlastContainer({ blastId }: EmailBlastContainerProps) {
|
|
|
130
130
|
</div>
|
|
131
131
|
<DetailField
|
|
132
132
|
label={t("emailBlasts.detail.sentAt")}
|
|
133
|
-
value={blast.sentAt ? blast.sentAt
|
|
133
|
+
value={blast.sentAt ? dates.format(blast.sentAt, "dateTimeMedium") : "—"}
|
|
134
134
|
/>
|
|
135
135
|
<DetailField label={t("emailBlasts.list.columns.subject")} value={blast.subject} />
|
|
136
136
|
<DetailField label={t("emailBlasts.detail.replyTo")} value={blast.replyTo ?? "—"} />
|
|
@@ -6,7 +6,7 @@ import { useMemo } from "react";
|
|
|
6
6
|
import { Badge, Link } from "../../../shadcnui";
|
|
7
7
|
import { Modules } from "../../../core";
|
|
8
8
|
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../hooks";
|
|
9
|
-
import {
|
|
9
|
+
import { useDateFormatter } from "../../../contexts/DateLocaleContext";
|
|
10
10
|
import { EmailBlastFields } from "../data/email-blast.fields";
|
|
11
11
|
import { EmailBlastInterface, EmailBlastStatus } from "../data/EmailBlastInterface";
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ const STATUS_VARIANTS: Record<EmailBlastStatus, "softGray" | "softAmber" | "soft
|
|
|
18
18
|
|
|
19
19
|
export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterface, EmailBlastFields> = (params) => {
|
|
20
20
|
const t = useTranslations();
|
|
21
|
-
const
|
|
21
|
+
const dates = useDateFormatter();
|
|
22
22
|
const generateUrl = usePageUrlGenerator();
|
|
23
23
|
|
|
24
24
|
const tableData = useMemo(() => {
|
|
@@ -90,7 +90,7 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
|
|
|
90
90
|
cell: ({ row }: { row: TableContent<EmailBlastInterface> }) => {
|
|
91
91
|
const blast: EmailBlastInterface = row.original.jsonApiData;
|
|
92
92
|
if (!blast.sentAt) return <span className="text-muted-foreground text-xs">—</span>;
|
|
93
|
-
return <span className="text-sm">{blast.sentAt
|
|
93
|
+
return <span className="text-sm">{dates.format(blast.sentAt, "date")}</span>;
|
|
94
94
|
},
|
|
95
95
|
enableSorting: false,
|
|
96
96
|
enableHiding: false,
|
|
@@ -111,7 +111,7 @@ export const useEmailBlastTableStructure: UseTableStructureHook<EmailBlastInterf
|
|
|
111
111
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
112
112
|
TableContent<EmailBlastInterface>
|
|
113
113
|
>[];
|
|
114
|
-
}, [params.fields, fieldColumnMap, t, generateUrl,
|
|
114
|
+
}, [params.fields, fieldColumnMap, t, generateUrl, dates]);
|
|
115
115
|
|
|
116
116
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
117
117
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
4
|
import { useMemo } from "react";
|
|
5
|
+
import { useDateFormatter } from "../../../../contexts/DateLocaleContext";
|
|
5
6
|
|
|
6
7
|
import { HandbookPageInterface } from "../../data/HandbookPageInterface";
|
|
7
8
|
|
|
@@ -27,7 +28,7 @@ const FAILURE_STATUSES = new Set(["failed", "error"]);
|
|
|
27
28
|
*/
|
|
28
29
|
export function HandbookIndexStatus({ pages }: HandbookIndexStatusProps) {
|
|
29
30
|
const t = useTranslations();
|
|
30
|
-
const
|
|
31
|
+
const dates = useDateFormatter();
|
|
31
32
|
|
|
32
33
|
const lastSynced = useMemo(() => {
|
|
33
34
|
let latest: Date | undefined = undefined;
|
|
@@ -50,7 +51,7 @@ export function HandbookIndexStatus({ pages }: HandbookIndexStatusProps) {
|
|
|
50
51
|
<span className="text-muted-foreground text-xs tabular-nums">
|
|
51
52
|
{t("handbook.contents.status", {
|
|
52
53
|
count: pages.length,
|
|
53
|
-
date: lastSynced ? format
|
|
54
|
+
date: lastSynced ? dates.format(lastSynced, "dateMedium") : "",
|
|
54
55
|
})}
|
|
55
56
|
{failures > 0 ? (
|
|
56
57
|
<span className="text-destructive ms-2 tabular-nums">
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import {
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
6
|
import { usePageUrlGenerator } from "../../../client";
|
|
7
|
+
import { useDateFormatter } from "../../../contexts/DateLocaleContext";
|
|
7
8
|
|
|
8
9
|
type Sibling = { howToType: string; slug: string; title: string };
|
|
9
10
|
|
|
@@ -17,7 +18,7 @@ export function HelpArticleBody(props: {
|
|
|
17
18
|
children: ReactNode;
|
|
18
19
|
}) {
|
|
19
20
|
const t = useTranslations();
|
|
20
|
-
const
|
|
21
|
+
const dates = useDateFormatter();
|
|
21
22
|
const generateUrl = usePageUrlGenerator();
|
|
22
23
|
const { howToType, title, summary, updatedAt, prev, next, children } = props;
|
|
23
24
|
|
|
@@ -36,7 +37,7 @@ export function HelpArticleBody(props: {
|
|
|
36
37
|
<div className="text-muted-foreground text-xs">
|
|
37
38
|
<span>
|
|
38
39
|
{t("help.article.lastUpdated", {
|
|
39
|
-
date: format
|
|
40
|
+
date: dates.format(updatedAt, "date"),
|
|
40
41
|
})}
|
|
41
42
|
</span>
|
|
42
43
|
</div>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import { ReactElement, useEffect, useState } from "react";
|
|
4
|
+
import { ReactElement, Suspense, useEffect, useState } from "react";
|
|
5
5
|
import { usePageUrlGenerator } from "../../../../hooks";
|
|
6
6
|
import { Link } from "../../../../shadcnui";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
DateLocaleSource,
|
|
9
|
+
DateLocaleSourceProvider,
|
|
10
|
+
useDateFormatter,
|
|
11
|
+
useMountedNow,
|
|
12
|
+
} from "../../../../contexts/DateLocaleContext";
|
|
13
|
+
import { formatDate } from "../../../../utils";
|
|
9
14
|
import { UserInterface } from "../../../user";
|
|
10
15
|
import { UserAvatar } from "../../../user/components";
|
|
11
16
|
import { NotificationInterface } from "../../data";
|
|
@@ -32,12 +37,22 @@ export const generateNotificationData = (params: {
|
|
|
32
37
|
return response;
|
|
33
38
|
};
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The toast's time. Sonner draws the toast outside the session providers, so the toast
|
|
42
|
+
* re-provides the captured date-locale source around this leaf, which suspends behind its
|
|
43
|
+
* own boundary.
|
|
44
|
+
*/
|
|
45
|
+
function NotificationToastTime({ date }: { date: Date }) {
|
|
46
|
+
const { format } = useDateFormatter();
|
|
47
|
+
return <>{format(new Date(date), "dateTime")}</>;
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
export function NotificationToast(
|
|
36
51
|
notification: NotificationInterface,
|
|
37
52
|
t: any,
|
|
38
53
|
generateUrl: any,
|
|
39
54
|
reouter: any,
|
|
40
|
-
|
|
55
|
+
dateLocaleSource: DateLocaleSource | null = null,
|
|
41
56
|
): {
|
|
42
57
|
title: string;
|
|
43
58
|
description: string | ReactElement<any>;
|
|
@@ -69,7 +84,11 @@ export function NotificationToast(
|
|
|
69
84
|
})}
|
|
70
85
|
</p>
|
|
71
86
|
<div className="text-muted-foreground mt-1 w-full text-xs">
|
|
72
|
-
{
|
|
87
|
+
<DateLocaleSourceProvider value={dateLocaleSource}>
|
|
88
|
+
<Suspense fallback={null}>
|
|
89
|
+
<NotificationToastTime date={notification.createdAt} />
|
|
90
|
+
</Suspense>
|
|
91
|
+
</DateLocaleSourceProvider>
|
|
73
92
|
</div>
|
|
74
93
|
</div>
|
|
75
94
|
</div>
|
|
@@ -85,11 +104,21 @@ export function NotificationToast(
|
|
|
85
104
|
};
|
|
86
105
|
}
|
|
87
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The popover row's time ("3 hours ago", else the date and time). A leaf behind its own
|
|
109
|
+
* boundary: the popover lives in the shell, and this read suspends on the session. The
|
|
110
|
+
* relative text renders only after mount, so the clock is never read during render.
|
|
111
|
+
*/
|
|
112
|
+
function NotificationMenuItemTime({ date }: { date: Date }) {
|
|
113
|
+
const { locale } = useDateFormatter();
|
|
114
|
+
const now = useMountedNow();
|
|
115
|
+
return <>{now ? formatDate(new Date(date), "default", locale, now) : ""}</>;
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
export function NotificationMenuItem({ notification, closePopover }: TaskCommentedOnProps) {
|
|
89
119
|
const generateUrl = usePageUrlGenerator();
|
|
90
120
|
const [isRead, setIsRead] = useState<boolean>(false);
|
|
91
121
|
const t = useTranslations();
|
|
92
|
-
const dateLocale = dateFormatLocale(useI18nLocale());
|
|
93
122
|
|
|
94
123
|
useEffect(() => {
|
|
95
124
|
setIsRead(notification.isRead);
|
|
@@ -115,7 +144,9 @@ export function NotificationMenuItem({ notification, closePopover }: TaskComment
|
|
|
115
144
|
})}
|
|
116
145
|
</p>
|
|
117
146
|
<div className="text-muted-foreground mt-0.5 text-xs">
|
|
118
|
-
{
|
|
147
|
+
<Suspense fallback={null}>
|
|
148
|
+
<NotificationMenuItemTime date={notification.createdAt} />
|
|
149
|
+
</Suspense>
|
|
119
150
|
</div>
|
|
120
151
|
</div>
|
|
121
152
|
{!isRead && <span className="bg-primary mt-1.5 size-2 shrink-0 rounded-full" aria-hidden />}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
4
|
import React, { createContext, ReactElement, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { SharedProvider } from "../../../contexts";
|
|
6
|
+
import { useDateLocaleSource } from "../../../contexts/DateLocaleContext";
|
|
6
7
|
import { Modules } from "../../../core";
|
|
7
|
-
import {
|
|
8
|
+
import { useI18nRouter } from "../../../i18n";
|
|
8
9
|
import { BreadcrumbItemData } from "../../../interfaces";
|
|
9
10
|
import { NotificationMenuItem, NotificationToast } from "../components/notifications/Notification";
|
|
10
11
|
import { getRoleId, isRolesConfigured } from "../../../roles";
|
|
@@ -47,7 +48,9 @@ type NotificationContextProviderProps = {
|
|
|
47
48
|
|
|
48
49
|
export const NotificationContextProvider = ({ children }: NotificationContextProviderProps) => {
|
|
49
50
|
const t = useTranslations();
|
|
50
|
-
|
|
51
|
+
// Non-suspending: the raw date-locale source, re-provided around the toast time (sonner
|
|
52
|
+
// renders the toast outside the session providers).
|
|
53
|
+
const dateLocaleSource = useDateLocaleSource();
|
|
51
54
|
// A ref, not a dependency: the translator only words the fallback errors and must
|
|
52
55
|
// not re-create loadNotifications, whose identity drives the initial-load effect.
|
|
53
56
|
const tRef = useRef(t);
|
|
@@ -181,7 +184,7 @@ export const NotificationContextProvider = ({ children }: NotificationContextPro
|
|
|
181
184
|
onClick: () => void;
|
|
182
185
|
};
|
|
183
186
|
} => {
|
|
184
|
-
return NotificationToast(notification, t, generateUrl, router,
|
|
187
|
+
return NotificationToast(notification, t, generateUrl, router, dateLocaleSource);
|
|
185
188
|
};
|
|
186
189
|
|
|
187
190
|
const generateNotification = (notification: NotificationInterface, closePopover: () => void) => {
|
|
@@ -6,8 +6,8 @@ import { useTranslations } from "next-intl";
|
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../hooks";
|
|
8
8
|
import { Button, Tooltip, TooltipContent, TooltipTrigger } from "../../../shadcnui";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { useDateFormatter, useMountedNow } from "../../../contexts/DateLocaleContext";
|
|
10
|
+
import { formatDate } from "../../../utils";
|
|
11
11
|
import { generateNotificationData } from "../components/notifications/Notification";
|
|
12
12
|
import { useNotificationContext } from "../contexts/NotificationContext";
|
|
13
13
|
import { NotificationIcon } from "../components/notifications/NotificationIcon";
|
|
@@ -33,7 +33,8 @@ export const useNotificationTableStructure: UseTableStructureHook<NotificationIn
|
|
|
33
33
|
params,
|
|
34
34
|
) => {
|
|
35
35
|
const t = useTranslations();
|
|
36
|
-
const dateLocale =
|
|
36
|
+
const { locale: dateLocale } = useDateFormatter();
|
|
37
|
+
const now = useMountedNow();
|
|
37
38
|
const generateUrl = usePageUrlGenerator();
|
|
38
39
|
const { loadNotifications } = useNotificationContext();
|
|
39
40
|
|
|
@@ -125,7 +126,7 @@ export const useNotificationTableStructure: UseTableStructureHook<NotificationIn
|
|
|
125
126
|
shows how long ago it arrived. The two must match. */
|
|
126
127
|
return (
|
|
127
128
|
<span className="text-muted-foreground text-xs">
|
|
128
|
-
{formatDate(new Date(notification.createdAt), "default", dateLocale)}
|
|
129
|
+
{now ? formatDate(new Date(notification.createdAt), "default", dateLocale, now) : ""}
|
|
129
130
|
</span>
|
|
130
131
|
);
|
|
131
132
|
},
|
|
@@ -175,7 +176,7 @@ export const useNotificationTableStructure: UseTableStructureHook<NotificationIn
|
|
|
175
176
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
176
177
|
TableContent<NotificationInterface>
|
|
177
178
|
>[];
|
|
178
|
-
}, [params.fields, fieldColumnMap, t, generateUrl]);
|
|
179
|
+
}, [params.fields, fieldColumnMap, t, generateUrl, dateLocale, now]);
|
|
179
180
|
|
|
180
181
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
181
182
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import {
|
|
5
|
-
import { useI18nDateFnsLocale } from "../../../i18n";
|
|
4
|
+
import { useDateFormatter, useMountedNow } from "../../../contexts/DateLocaleContext";
|
|
6
5
|
import { Key, MoreVertical, Pencil, Trash2 } from "lucide-react";
|
|
7
6
|
import {
|
|
8
7
|
Badge,
|
|
@@ -35,13 +34,17 @@ export interface OAuthClientCardProps {
|
|
|
35
34
|
*/
|
|
36
35
|
export function OAuthClientCard({ client, onClick, onEdit, onDelete }: OAuthClientCardProps) {
|
|
37
36
|
const t = useTranslations();
|
|
38
|
-
const
|
|
37
|
+
const { relative } = useDateFormatter();
|
|
38
|
+
const now = useMountedNow();
|
|
39
39
|
// Truncate client ID for display
|
|
40
40
|
const truncatedId =
|
|
41
41
|
client.clientId.length > 12 ? `${client.clientId.slice(0, 8)}...${client.clientId.slice(-4)}` : client.clientId;
|
|
42
42
|
|
|
43
|
+
// The relative text renders only after mount: `now` is never read during render.
|
|
43
44
|
const createdAgo = client.createdAt
|
|
44
|
-
?
|
|
45
|
+
? now
|
|
46
|
+
? relative(new Date(client.createdAt), { now })
|
|
47
|
+
: undefined
|
|
45
48
|
: t("oauth.ui.card.unknown");
|
|
46
49
|
|
|
47
50
|
return (
|
|
@@ -100,7 +103,7 @@ export function OAuthClientCard({ client, onClick, onEdit, onDelete }: OAuthClie
|
|
|
100
103
|
<CardContent>
|
|
101
104
|
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
|
102
105
|
<span className="tabular-nums">{truncatedId}</span>
|
|
103
|
-
<span>{t("oauth.ui.card.created", { ago: createdAgo })}</span>
|
|
106
|
+
{createdAgo !== undefined && <span>{t("oauth.ui.card.created", { ago: createdAgo })}</span>}
|
|
104
107
|
<span>{client.isConfidential ? t("oauth.ui.card.confidential") : t("oauth.ui.card.public")}</span>
|
|
105
108
|
</div>
|
|
106
109
|
</CardContent>
|
|
@@ -23,8 +23,7 @@ import {
|
|
|
23
23
|
AlertDialogHeader,
|
|
24
24
|
AlertDialogTitle,
|
|
25
25
|
} from "../../../shadcnui";
|
|
26
|
-
import {
|
|
27
|
-
import { dateFormatLocale } from "../../../utils";
|
|
26
|
+
import { useDateFormatter } from "../../../contexts/DateLocaleContext";
|
|
28
27
|
import { useOAuthScopeText } from "../hooks/useOAuthScopeText";
|
|
29
28
|
import { OAuthClientInterface } from "../interfaces/oauth.interface";
|
|
30
29
|
|
|
@@ -52,7 +51,7 @@ export function OAuthClientDetail({
|
|
|
52
51
|
onRegenerateSecret,
|
|
53
52
|
}: OAuthClientDetailProps) {
|
|
54
53
|
const t = useTranslations();
|
|
55
|
-
const
|
|
54
|
+
const { format } = useDateFormatter();
|
|
56
55
|
const scopeText = useOAuthScopeText();
|
|
57
56
|
const [copiedField, setCopiedField] = useState<string | null>(null);
|
|
58
57
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
@@ -92,9 +91,7 @@ export function OAuthClientDetail({
|
|
|
92
91
|
}
|
|
93
92
|
}, [onRegenerateSecret]);
|
|
94
93
|
|
|
95
|
-
const createdDate = client.createdAt
|
|
96
|
-
? new Date(client.createdAt).toLocaleDateString(dateFormatLocale(locale), { dateStyle: "long" })
|
|
97
|
-
: t("oauth.ui.card.unknown");
|
|
94
|
+
const createdDate = client.createdAt ? format(new Date(client.createdAt), "dateLong") : t("oauth.ui.card.unknown");
|
|
98
95
|
|
|
99
96
|
return (
|
|
100
97
|
<>
|
|
@@ -19,3 +19,22 @@ describe("createUsageFormatters.compact", () => {
|
|
|
19
19
|
expect(compact(1500000)).toBe("1.5M");
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
+
|
|
23
|
+
describe("createUsageFormatters.bucketDate dateLocale", () => {
|
|
24
|
+
it("formats day buckets with the date locale", () => {
|
|
25
|
+
expect(createUsageFormatters("en", "EUR", "en-GB").bucketDate("2026-04-25", "day")).toBe("25 Apr");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("defaults the date locale to the number locale", () => {
|
|
29
|
+
expect(createUsageFormatters("en", "EUR").bucketDate("2026-04-25", "day")).toBe("Apr 25");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps number output identical between the two", () => {
|
|
33
|
+
const withDateLocale = createUsageFormatters("en", "EUR", "en-GB");
|
|
34
|
+
const withoutDateLocale = createUsageFormatters("en", "EUR");
|
|
35
|
+
for (const value of [0, 0.05, 1250, 1500000]) {
|
|
36
|
+
expect(withDateLocale.compact(value)).toBe(withoutDateLocale.compact(value));
|
|
37
|
+
expect(withDateLocale.decimal(value, 2)).toBe(withoutDateLocale.decimal(value, 2));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useLocale } from "next-intl";
|
|
4
4
|
import { useMemo } from "react";
|
|
5
|
+
import { useDateFormatter } from "../../../contexts/DateLocaleContext";
|
|
5
6
|
import type { Metric } from "../data/tokenusage-admin.types";
|
|
6
7
|
import { getTokenUsageCurrency } from "./config";
|
|
7
8
|
|
|
@@ -38,8 +39,11 @@ export type UsageFormatters = {
|
|
|
38
39
|
* `bucketDate` formats in UTC, always. The wire value is a `type: "date"` — a
|
|
39
40
|
* calendar day — parsed to UTC midnight; reading it back with local getters
|
|
40
41
|
* would shift the label a day early west of UTC.
|
|
42
|
+
*
|
|
43
|
+
* `dateLocale` formats the bucket dates only (the browser's regional order);
|
|
44
|
+
* numbers, currency and collation keep `locale`. It defaults to `locale`.
|
|
41
45
|
*/
|
|
42
|
-
export function createUsageFormatters(locale: string, currency: string): UsageFormatters {
|
|
46
|
+
export function createUsageFormatters(locale: string, currency: string, dateLocale: string = locale): UsageFormatters {
|
|
43
47
|
const symbol =
|
|
44
48
|
new Intl.NumberFormat(locale, { style: "currency", currency, currencyDisplay: "narrowSymbol" })
|
|
45
49
|
.formatToParts(0)
|
|
@@ -48,8 +52,8 @@ export function createUsageFormatters(locale: string, currency: string): UsageFo
|
|
|
48
52
|
const decimal = (value: number, decimals: number): string =>
|
|
49
53
|
value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
50
54
|
|
|
51
|
-
const dayFormat = new Intl.DateTimeFormat(
|
|
52
|
-
const monthFormat = new Intl.DateTimeFormat(
|
|
55
|
+
const dayFormat = new Intl.DateTimeFormat(dateLocale, { day: "numeric", month: "short", timeZone: "UTC" });
|
|
56
|
+
const monthFormat = new Intl.DateTimeFormat(dateLocale, { month: "short", year: "numeric", timeZone: "UTC" });
|
|
53
57
|
// Axis ticks step by 1, 2, 2.5 or 5 times a power of ten, so a tick never needs
|
|
54
58
|
// more than three significant digits. A fixed one-decimal cap rounded a 0.05
|
|
55
59
|
// step into repeated labels (0.1, 0.1, 0.2, 0.2); significant digits keep every
|
|
@@ -94,11 +98,12 @@ export function createUsageFormatters(locale: string, currency: string): UsageFo
|
|
|
94
98
|
|
|
95
99
|
/**
|
|
96
100
|
* The formatter set for the current request's locale and the app's configured
|
|
97
|
-
* currency
|
|
98
|
-
* rather than on every render.
|
|
101
|
+
* currency, with bucket dates in the resolved date locale. Memoised on all
|
|
102
|
+
* three, so the Intl objects are built once per locale rather than on every render.
|
|
99
103
|
*/
|
|
100
104
|
export function useUsageFormatters(): UsageFormatters {
|
|
101
105
|
const locale = useLocale();
|
|
106
|
+
const { locale: dateLocale } = useDateFormatter();
|
|
102
107
|
const currency = getTokenUsageCurrency();
|
|
103
|
-
return useMemo(() => createUsageFormatters(locale, currency), [locale, currency]);
|
|
108
|
+
return useMemo(() => createUsageFormatters(locale, currency, dateLocale), [locale, currency, dateLocale]);
|
|
104
109
|
}
|