diff --git a/AGENTS.md b/AGENTS.md index e6dfd8792..370ec59d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,7 @@ - one file can have many components - all texts should be provided translations via the i18next library's `useTranslations` hook's `t` function - instead of `&&` operator for conditional rendering, use the ternary operator +- for localized user-readable time strings use ``, `` or `useFormatDistanceToNow`. If needed use `useDateTimeFormat` directly. NEVER use e.g. `toLocaleString` directly as it does not include users' language selection. ## Remix/React Router diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx index 26c9f6ed8..b70079530 100644 --- a/app/components/BuildCard.tsx +++ b/app/components/BuildCard.tsx @@ -5,15 +5,12 @@ import { Link } from "react-router"; import type { GearType, Tables, UserWithPlusTier } from "~/db/tables"; import { useUser } from "~/features/auth/core/user"; import type { BuildWeaponWithTop500Info } from "~/features/builds/builds-types"; -import { useHydrated } from "~/hooks/useHydrated"; -import { useTimeFormat } from "~/hooks/useTimeFormat"; import type { Ability as AbilityType, BuildAbilitiesTuple, ModeShort, } from "~/modules/in-game-lists/types"; import { altWeaponIdToId } from "~/modules/in-game-lists/weapon-ids"; -import { databaseTimestampToDate } from "~/utils/dates"; import { gearTypeToInitial } from "~/utils/strings"; import { analyzerPage, @@ -31,6 +28,7 @@ import { LinkButton, SendouButton } from "./elements/Button"; import { SendouPopover } from "./elements/Popover"; import { FormWithConfirm } from "./FormWithConfirm"; import { Image } from "./Image"; +import { LocaleTime } from "./LocaleTime"; interface BuildProps { build: Pick< @@ -55,8 +53,6 @@ interface BuildProps { export function BuildCard({ build, owner, canEdit = false }: BuildProps) { const user = useUser(); const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]); - const { formatDate } = useTimeFormat(); - const isHydrated = useHydrated(); const { id, @@ -122,17 +118,15 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) { {t("common:build.private")} ) : null} - + diff --git a/app/components/Chart.tsx b/app/components/Chart.tsx index 3fe9fc542..8dfafc8b4 100644 --- a/app/components/Chart.tsx +++ b/app/components/Chart.tsx @@ -3,8 +3,8 @@ import * as React from "react"; import { type AxisOptions, Chart as ReactChart } from "react-charts"; import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer"; import { Theme, useTheme } from "~/features/theme/core/provider"; +import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { useHydrated } from "~/hooks/useHydrated"; -import { useTimeFormat } from "~/hooks/useTimeFormat"; import styles from "./Chart.module.css"; export default function Chart({ @@ -24,7 +24,10 @@ export default function Chart({ }) { const theme = useTheme(); const isHydrated = useHydrated(); - const { formatDate } = useTimeFormat(); + const { formatter: scaleFormatter } = useDateTimeFormat({ + day: "numeric", + month: "numeric", + }); const primaryAxis = React.useMemo< AxisOptions<(typeof options)[number]["data"][number]> @@ -37,17 +40,14 @@ export default function Chart({ formatters: { scale: (val: any) => { if (val instanceof Date) { - return formatDate(val, { - day: "numeric", - month: "numeric", - }); + return scaleFormatter.format(val); } return val; }, }, }), - [formatDate, xAxis], + [scaleFormatter, xAxis], ); const secondaryAxes = React.useMemo< @@ -105,7 +105,11 @@ function ChartTooltip({ headerSuffix = "", valueSuffix = "", }: ChartTooltipProps) { - const { formatDate } = useTimeFormat(); + const { formatter: headerFormatter } = useDateTimeFormat({ + weekday: "short", + day: "numeric", + month: "numeric", + }); const dataPoints = focusedDatum?.interactiveGroup ?? []; const header = () => { @@ -113,11 +117,7 @@ function ChartTooltip({ if (!primaryValue) return null; if (primaryValue instanceof Date) { - return formatDate(primaryValue, { - weekday: "short", - day: "numeric", - month: "numeric", - }); + return headerFormatter.format(primaryValue); } return primaryValue; diff --git a/app/components/EventsList.tsx b/app/components/EventsList.tsx index a8b57200b..6ada6f1aa 100644 --- a/app/components/EventsList.tsx +++ b/app/components/EventsList.tsx @@ -1,8 +1,8 @@ import { isToday, isTomorrow } from "date-fns"; import { useTranslation } from "react-i18next"; import type { SidebarEvent } from "~/features/sidebar/core/sidebar.server"; +import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { useHydrated } from "~/hooks/useHydrated"; -import { useTimeFormat } from "~/hooks/useTimeFormat"; import styles from "./EventsList.module.css"; import { Placeholder } from "./Placeholder"; import { ListLink } from "./SideNav"; @@ -15,7 +15,15 @@ export function EventsList({ onClick?: () => void; }) { const { t, i18n } = useTranslation(["front"]); - const { formatDate, formatTime } = useTimeFormat(); + const { formatter: dateFormatter } = useDateTimeFormat({ + weekday: "long", + month: "numeric", + day: "numeric", + }); + const { formatter: timeFormatter } = useDateTimeFormat({ + hour: "numeric", + minute: "2-digit", + }); const isHydrated = useHydrated(); if (events.length === 0) { @@ -50,11 +58,7 @@ export function EventsList({ const str = rtf.format(1, "day"); return str.charAt(0).toUpperCase() + str.slice(1); } - return formatDate(date, { - weekday: "long", - month: "numeric", - day: "numeric", - }); + return dateFormatter.format(date); }; const groupedEvents = events.reduce>( @@ -85,7 +89,7 @@ export function EventsList({ key={`${event.type}-${event.id}`} to={event.url} imageUrl={event.logoUrl ?? undefined} - subtitle={formatTime(new Date(event.startTime * 1000))} + subtitle={timeFormatter.format(event.startTime)} onClick={onClick} > {event.scrimStatus === "booked" diff --git a/app/components/LocaleTime.tsx b/app/components/LocaleTime.tsx new file mode 100644 index 000000000..4aee47ec6 --- /dev/null +++ b/app/components/LocaleTime.tsx @@ -0,0 +1,49 @@ +import clsx from "clsx"; +import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; +import { databaseTimestampToDate } from "~/utils/dates"; + +interface LocaleTimeProps { + /** The date to render. Accepts a `Date` or a database timestamp (number), which is converted via `databaseTimestampToDate`. */ + date: Date | number; + /** Formatting options forwarded to `Intl.DateTimeFormat`. Combined with the user's locale and hour cycle preferences. */ + options: Intl.DateTimeFormatOptions; + /** Optional extra class names appended to the rendered `