From 6a88daf921ac55fd92bbadd712e96bfcddea104d Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:54:37 +0200 Subject: [PATCH] Add relative time to recent results, support i18n in formatDistance Closes #2418 --- app/features/art/components/ArtGrid.tsx | 2 +- .../calendar/components/TournamentCard.tsx | 11 +++- app/features/front-page/routes/index.tsx | 6 +- app/features/lfg/components/LFGPost.tsx | 3 +- app/hooks/useTimeFormat.ts | 13 ++++- app/utils/dates.ts | 58 ++++++++++++++++++- 6 files changed, 86 insertions(+), 7 deletions(-) diff --git a/app/features/art/components/ArtGrid.tsx b/app/features/art/components/ArtGrid.tsx index aaf9eebe5..9ebec8d34 100644 --- a/app/features/art/components/ArtGrid.tsx +++ b/app/features/art/components/ArtGrid.tsx @@ -1,6 +1,5 @@ import { Link } from "@remix-run/react"; import clsx from "clsx"; -import { formatDistanceToNow } from "date-fns"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Avatar } from "~/components/Avatar"; @@ -171,6 +170,7 @@ function ImagePreview({ }) { const [imageLoaded, setImageLoaded] = React.useState(false); const { t } = useTranslation(["common", "art"]); + const { formatDistanceToNow } = useTimeFormat(); const img = ( // biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration diff --git a/app/features/calendar/components/TournamentCard.tsx b/app/features/calendar/components/TournamentCard.tsx index 046219ee9..66ad66cba 100644 --- a/app/features/calendar/components/TournamentCard.tsx +++ b/app/features/calendar/components/TournamentCard.tsx @@ -19,12 +19,14 @@ import styles from "./TournamentCard.module.css"; export function TournamentCard({ tournament, className, + withRelativeTime = false, }: { tournament: CalendarEvent | ShowcaseCalendarEvent; className?: string; + withRelativeTime?: boolean; }) { const isMounted = useIsMounted(); - const { formatDateTime } = useTimeFormat(); + const { formatDateTime, formatDistanceToNow } = useTimeFormat(); const isShowcase = tournament.type === "showcase"; const isCalendar = tournament.type === "calendar"; @@ -35,6 +37,13 @@ export function TournamentCard({ if (!isMounted) return "Placeholder"; const date = databaseTimestampToDate(tournament.startTime); + + if (withRelativeTime) { + return formatDistanceToNow(date, { + addSuffix: true, + }); + } + return formatDateTime(date, { month: "short", day: "numeric", diff --git a/app/features/front-page/routes/index.tsx b/app/features/front-page/routes/index.tsx index 7050fb50b..dfc2ba75d 100644 --- a/app/features/front-page/routes/index.tsx +++ b/app/features/front-page/routes/index.tsx @@ -283,7 +283,11 @@ function ResultHighlights() {
{data.tournaments.results.map((tournament) => ( - + ))}
diff --git a/app/features/lfg/components/LFGPost.tsx b/app/features/lfg/components/LFGPost.tsx index ab4bce79e..2c86e5671 100644 --- a/app/features/lfg/components/LFGPost.tsx +++ b/app/features/lfg/components/LFGPost.tsx @@ -1,6 +1,5 @@ import { Link, useFetcher } from "@remix-run/react"; import clsx from "clsx"; -import { formatDistanceToNow } from "date-fns"; import React from "react"; import { useTranslation } from "react-i18next"; import { Avatar } from "~/components/Avatar"; @@ -260,7 +259,7 @@ function PostTime({ updatedAt: number; }) { const { t } = useTranslation(["lfg"]); - const { formatDate } = useTimeFormat(); + const { formatDate, formatDistanceToNow } = useTimeFormat(); const createdAtDate = databaseTimestampToDate(createdAt); const updatedAtDate = databaseTimestampToDate(updatedAt); diff --git a/app/hooks/useTimeFormat.ts b/app/hooks/useTimeFormat.ts index 9b2db5143..bb44647e0 100644 --- a/app/hooks/useTimeFormat.ts +++ b/app/hooks/useTimeFormat.ts @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next"; import { useUser } from "~/features/auth/core/user"; +import { formatDistanceToNow as formatDistanceToNowUtil } from "~/utils/dates"; const H12_TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour12: true, @@ -94,7 +95,17 @@ export function useTimeFormat() { return date.toLocaleDateString(i18n.language, options); }; - return { formatDateTime, formatTime, formatDate }; + const formatDistanceToNow = ( + date: Parameters[0], + options?: Omit[1], "language">, + ) => { + return formatDistanceToNowUtil(date, { + ...options, + language: i18n.language, + }); + }; + + return { formatDateTime, formatTime, formatDate, formatDistanceToNow }; } // Example: "09:00" -> "9:00" diff --git a/app/utils/dates.ts b/app/utils/dates.ts index b7e7d6d3f..98eb31731 100644 --- a/app/utils/dates.ts +++ b/app/utils/dates.ts @@ -1,8 +1,64 @@ import { CalendarDateTime, parseDate } from "@internationalized/date"; -import { getWeek } from "date-fns"; +import type { Locale } from "date-fns"; +import { + formatDistanceToNow as dateFnsFormatDistanceToNow, + getWeek, +} from "date-fns"; +import { da } from "date-fns/locale/da"; +import { de } from "date-fns/locale/de"; +import { enUS } from "date-fns/locale/en-US"; +import { es } from "date-fns/locale/es"; +import { fr } from "date-fns/locale/fr"; +import { frCA } from "date-fns/locale/fr-CA"; +import { he } from "date-fns/locale/he"; +import { it } from "date-fns/locale/it"; +import { ja } from "date-fns/locale/ja"; +import { ko } from "date-fns/locale/ko"; +import { nl } from "date-fns/locale/nl"; +import { pl } from "date-fns/locale/pl"; +import { ptBR } from "date-fns/locale/pt-BR"; +import { ru } from "date-fns/locale/ru"; +import { zhCN } from "date-fns/locale/zh-CN"; import type { MonthYear } from "~/features/plus-voting/core"; +import type { LanguageCode } from "~/modules/i18n/config"; import type { DayMonthYear } from "./zod"; +const LOCALE_MAP: Record = { + da, + de, + en: enUS, + "es-ES": es, + "es-US": es, + "fr-CA": frCA, + "fr-EU": fr, + he, + it, + ja, + ko, + nl, + pl, + "pt-BR": ptBR, + ru, + zh: zhCN, +}; + +export function getDateFnsLocale(language: string) { + return LOCALE_MAP[language as keyof typeof LOCALE_MAP] ?? enUS; +} + +export function formatDistanceToNow( + date: Parameters[0], + options: Omit< + NonNullable[1]>, + "locale" + > & { language: string }, +) { + return dateFnsFormatDistanceToNow(date, { + ...options, + locale: getDateFnsLocale(options.language), + }); +} + export function databaseTimestampToDate(timestamp: number) { return new Date(databaseTimestampToJavascriptTimestamp(timestamp)); }