diff --git a/frontend/src/components/Form/AccessField.tsx b/frontend/src/components/Form/AccessField.tsx index c72ef588..16059ac3 100644 --- a/frontend/src/components/Form/AccessField.tsx +++ b/frontend/src/components/Form/AccessField.tsx @@ -4,7 +4,7 @@ import type { ReactNode } from "react"; import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import type { AccessList } from "src/api/backend"; import { useAccessLists } from "src/hooks"; -import { DateTimeFormat, intl, T } from "src/locale"; +import { formatDateTime, intl, T } from "src/locale"; interface AccessOption { readonly value: number; @@ -48,7 +48,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id = { users: item?.items?.length, rules: item?.clients?.length, - date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A", + date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A", }, ), icon: , diff --git a/frontend/src/components/Form/SSLCertificateField.tsx b/frontend/src/components/Form/SSLCertificateField.tsx index c4767509..0e7ce337 100644 --- a/frontend/src/components/Form/SSLCertificateField.tsx +++ b/frontend/src/components/Form/SSLCertificateField.tsx @@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik"; import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import type { Certificate } from "src/api/backend"; import { useCertificates } from "src/hooks"; -import { DateTimeFormat, intl, T } from "src/locale"; +import { formatDateTime, intl, T } from "src/locale"; interface CertOption { readonly value: number | "new"; @@ -75,7 +75,7 @@ export function SSLCertificateField({ data?.map((cert: Certificate) => ({ value: cert.id, label: cert.niceName, - subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A" })}`, + subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`, icon: , })) || []; diff --git a/frontend/src/components/Table/Formatter/DateFormatter.tsx b/frontend/src/components/Table/Formatter/DateFormatter.tsx index e8c2c6c7..acb96461 100644 --- a/frontend/src/components/Table/Formatter/DateFormatter.tsx +++ b/frontend/src/components/Table/Formatter/DateFormatter.tsx @@ -1,6 +1,6 @@ import cn from "classnames"; -import { differenceInDays, isPast, parseISO } from "date-fns"; -import { DateTimeFormat } from "src/locale"; +import { differenceInDays, isPast } from "date-fns"; +import { formatDateTime, parseDate } from "src/locale"; interface Props { value: string; @@ -8,11 +8,12 @@ interface Props { highlistNearlyExpired?: boolean; } export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) { - const dateIsPast = isPast(parseISO(value)); - const days = differenceInDays(parseISO(value), new Date()); + const d = parseDate(value); + const dateIsPast = d ? isPast(d) : false; + const days = d ? differenceInDays(d, new Date()) : 0; const cl = cn({ "text-danger": highlightPast && dateIsPast, "text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0, }); - return {DateTimeFormat(value)}; + return {formatDateTime(value)}; } diff --git a/frontend/src/components/Table/Formatter/DomainsFormatter.tsx b/frontend/src/components/Table/Formatter/DomainsFormatter.tsx index 8560eae2..4cc50475 100644 --- a/frontend/src/components/Table/Formatter/DomainsFormatter.tsx +++ b/frontend/src/components/Table/Formatter/DomainsFormatter.tsx @@ -1,6 +1,6 @@ import cn from "classnames"; import type { ReactNode } from "react"; -import { DateTimeFormat, T } from "src/locale"; +import { formatDateTime, T } from "src/locale"; interface Props { domains: string[]; @@ -53,7 +53,7 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
{...elms}
{createdOn ? (
- +
) : null} diff --git a/frontend/src/components/Table/Formatter/EventFormatter.tsx b/frontend/src/components/Table/Formatter/EventFormatter.tsx index f6f7787d..ff37ecbe 100644 --- a/frontend/src/components/Table/Formatter/EventFormatter.tsx +++ b/frontend/src/components/Table/Formatter/EventFormatter.tsx @@ -1,7 +1,7 @@ import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react"; import cn from "classnames"; import type { AuditLog } from "src/api/backend"; -import { DateTimeFormat, T } from "src/locale"; +import { formatDateTime, T } from "src/locale"; const getEventValue = (event: AuditLog) => { switch (event.objectType) { @@ -73,7 +73,7 @@ export function EventFormatter({ row }: Props) {   — {getEventValue(row)} -
{DateTimeFormat(row.createdOn)}
+
{formatDateTime(row.createdOn)}
); } diff --git a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx index e4e7fb27..38352b31 100644 --- a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx +++ b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx @@ -1,4 +1,4 @@ -import { DateTimeFormat, T } from "src/locale"; +import { formatDateTime, T } from "src/locale"; interface Props { value: string; @@ -13,7 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) { {createdOn ? (
- +
) : null} diff --git a/frontend/src/components/Table/Formatter/DateFormatter.test.tsx b/frontend/src/locale/Utils.test.tsx similarity index 81% rename from frontend/src/components/Table/Formatter/DateFormatter.test.tsx rename to frontend/src/locale/Utils.test.tsx index 33c561c7..fb262501 100644 --- a/frontend/src/components/Table/Formatter/DateFormatter.test.tsx +++ b/frontend/src/locale/Utils.test.tsx @@ -1,4 +1,4 @@ -import { DateTimeFormat } from "src/locale"; +import { formatDateTime } from "src/locale"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; describe("DateFormatter", () => { @@ -17,10 +17,7 @@ describe("DateFormatter", () => { // Mock Intl.DateTimeFormat so formatting is stable regardless of host const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat { - constructor( - _locales?: string | string[], - options?: Intl.DateTimeFormatOptions, - ) { + constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) { super(desiredLocale, { ...options, timeZone: desiredTimeZone, @@ -41,37 +38,37 @@ describe("DateFormatter", () => { it("format date from iso date", () => { const value = "2024-01-01T00:00:00.000Z"; - const text = DateTimeFormat(value); + const text = formatDateTime(value); expect(text).toBe("Monday, 01/01/2024, 12:00:00 am"); }); it("format date from unix timestamp number", () => { const value = 1762476112; - const text = DateTimeFormat(value); + const text = formatDateTime(value); expect(text).toBe("Friday, 07/11/2025, 12:41:52 am"); }); it("format date from unix timestamp string", () => { const value = "1762476112"; - const text = DateTimeFormat(value); + const text = formatDateTime(value); expect(text).toBe("Friday, 07/11/2025, 12:41:52 am"); }); it("catch bad format from string", () => { const value = "this is not a good date"; - const text = DateTimeFormat(value); + const text = formatDateTime(value); expect(text).toBe("this is not a good date"); }); it("catch bad format from number", () => { const value = -100; - const text = DateTimeFormat(value); + const text = formatDateTime(value); expect(text).toBe("-100"); }); it("catch bad format from number as string", () => { const value = "-100"; - const text = DateTimeFormat(value); + const text = formatDateTime(value); expect(text).toBe("-100"); }); }); diff --git a/frontend/src/locale/DateTimeFormat.ts b/frontend/src/locale/Utils.ts similarity index 70% rename from frontend/src/locale/DateTimeFormat.ts rename to frontend/src/locale/Utils.ts index 93c2121d..018efd0b 100644 --- a/frontend/src/locale/DateTimeFormat.ts +++ b/frontend/src/locale/Utils.ts @@ -11,13 +11,19 @@ const isUnixTimestamp = (value: unknown): boolean => { return false; }; -const DateTimeFormat = (value: string | number): string => { - if (typeof value !== "number" && typeof value !== "string") return `${value}`; - +const parseDate = (value: string | number): Date | null => { + if (typeof value !== "number" && typeof value !== "string") return null; + try { + return isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`); + } catch { + return null; + } +}; + +const formatDateTime = (value: string | number): string => { + const d = parseDate(value); + if (!d) return `${value}`; try { - const d = isUnixTimestamp(value) - ? fromUnixTime(+value) - : parseISO(`${value}`); return intlFormat(d, { weekday: "long", year: "numeric", @@ -33,4 +39,4 @@ const DateTimeFormat = (value: string | number): string => { } }; -export { DateTimeFormat }; +export { formatDateTime, parseDate, isUnixTimestamp }; diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts index 6d9ac03c..bdd1343e 100644 --- a/frontend/src/locale/index.ts +++ b/frontend/src/locale/index.ts @@ -1,2 +1,2 @@ -export * from "./DateTimeFormat"; export * from "./IntlProvider"; +export * from "./Utils";