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));
}