Make date time format apply everywhere

This commit is contained in:
Kalle 2026-04-30 17:05:12 +03:00
parent bfef81682e
commit d1f1ac7460
44 changed files with 235 additions and 171 deletions

View File

@ -128,7 +128,7 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
{isHydrated
? formatDate(databaseTimestampToDate(updatedAt), {
day: "numeric",
month: "long",
month: "numeric",
year: "numeric",
})
: "t"}

View File

@ -2,7 +2,6 @@ import clsx from "clsx";
import * as React from "react";
import { type AxisOptions, Chart as ReactChart } from "react-charts";
import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer";
import { useTranslation } from "react-i18next";
import { Theme, useTheme } from "~/features/theme/core/provider";
import { useHydrated } from "~/hooks/useHydrated";
import { useTimeFormat } from "~/hooks/useTimeFormat";
@ -23,9 +22,9 @@ export default function Chart({
valueSuffix?: string;
xAxis: "linear" | "localTime";
}) {
const { i18n } = useTranslation();
const theme = useTheme();
const isHydrated = useHydrated();
const { formatDate } = useTimeFormat();
const primaryAxis = React.useMemo<
AxisOptions<(typeof options)[number]["data"][number]>
@ -38,7 +37,7 @@ export default function Chart({
formatters: {
scale: (val: any) => {
if (val instanceof Date) {
return val.toLocaleDateString(i18n.language, {
return formatDate(val, {
day: "numeric",
month: "numeric",
});
@ -48,7 +47,7 @@ export default function Chart({
},
},
}),
[i18n.language, xAxis],
[formatDate, xAxis],
);
const secondaryAxes = React.useMemo<
@ -117,7 +116,7 @@ function ChartTooltip({
return formatDate(primaryValue, {
weekday: "short",
day: "numeric",
month: "long",
month: "numeric",
});
}

View File

@ -2,6 +2,7 @@ import { isToday, isTomorrow } from "date-fns";
import { useTranslation } from "react-i18next";
import type { SidebarEvent } from "~/features/sidebar/core/sidebar.server";
import { useHydrated } from "~/hooks/useHydrated";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import styles from "./EventsList.module.css";
import { Placeholder } from "./Placeholder";
import { ListLink } from "./SideNav";
@ -14,6 +15,7 @@ export function EventsList({
onClick?: () => void;
}) {
const { t, i18n } = useTranslation(["front"]);
const { formatDate, formatTime } = useTimeFormat();
const isHydrated = useHydrated();
if (events.length === 0) {
@ -48,20 +50,13 @@ export function EventsList({
const str = rtf.format(1, "day");
return str.charAt(0).toUpperCase() + str.slice(1);
}
return date.toLocaleDateString(i18n.language, {
return formatDate(date, {
weekday: "long",
month: "short",
month: "numeric",
day: "numeric",
});
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "2-digit",
});
};
const groupedEvents = events.reduce<Record<string, typeof events>>(
(acc, event) => {
const key = getDayKey(event.startTime);

View File

@ -20,7 +20,7 @@ export function RelativeTime({
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
month: "numeric",
timeZoneName: "short",
})
: undefined

View File

@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next";
import { useFetcher } from "react-router";
import type { SidebarStream } from "~/features/core/streams/streams.server";
import { useHydrated } from "~/hooks/useHydrated";
import type { LanguageCode } from "~/modules/i18n/config";
import { databaseTimestampToDate, formatDistanceToNow } from "~/utils/dates";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { databaseTimestampToDate } from "~/utils/dates";
import { navIconUrl, tournamentRegisterPage } from "~/utils/urls";
import { Image } from "./Image";
import { ListLink } from "./SideNav";
@ -25,14 +25,12 @@ export function StreamListItems({
savedTournamentIds?: number[];
}) {
const { t, i18n } = useTranslation(["front"]);
const { formatDateTime, formatTime, formatDistanceToNow } = useTimeFormat();
const isHydrated = useHydrated();
const formatRelativeDate = (timestamp: number) => {
const date = new Date(timestamp * 1000);
const timeStr = date.toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "2-digit",
});
const timeStr = formatTime(date);
if (isToday(date)) {
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
@ -49,8 +47,8 @@ export function StreamListItems({
return `${dayStr.charAt(0).toUpperCase() + dayStr.slice(1)}, ${timeStr}`;
}
return date.toLocaleDateString(i18n.language, {
month: "short",
return formatDateTime(date, {
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
@ -101,7 +99,6 @@ export function StreamListItems({
) : (
formatDistanceToNow(startsAtDate, {
addSuffix: true,
language: i18n.language as LanguageCode,
})
)
}

View File

@ -16,7 +16,7 @@ export default function TimePopover({
minute: "numeric",
hour: "numeric",
day: "numeric",
month: "long",
month: "numeric",
},
underline = true,
className,

View File

@ -1,5 +1,5 @@
import clsx from "clsx";
import { format, sub } from "date-fns";
import { sub } from "date-fns";
import { ChevronsUpDown, Search, X } from "lucide-react";
import * as React from "react";
import {
@ -21,6 +21,7 @@ import { useDebounce } from "react-use";
import { SendouBottomTexts } from "~/components/elements/BottomTexts";
import { SendouLabel } from "~/components/elements/Label";
import type { TournamentSearchLoaderData } from "~/features/tournament/routes/to.search";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { databaseTimestampToDate } from "~/utils/dates";
import selectStyles from "./Select.module.css";
@ -150,6 +151,7 @@ function TournamentItem({
};
}) {
const { t } = useTranslation(["common"]);
const { formatDate } = useTimeFormat();
if (typeof item.id === "string") {
return (
@ -167,7 +169,11 @@ function TournamentItem({
const additionalText = () => {
const date = databaseTimestampToDate(item.startTime);
return format(date, "MMM d, yyyy");
return formatDate(date, {
day: "numeric",
month: "numeric",
year: "numeric",
});
};
return (
@ -185,11 +191,9 @@ function TournamentItem({
<img src={item.logoUrl} alt="" className={tournamentSearchStyles.logo} />
<div className={tournamentSearchStyles.itemTextsContainer}>
<span>{item.name}</span>
{additionalText() ? (
<div className={tournamentSearchStyles.itemAdditionalText}>
{additionalText()}
</div>
) : null}
<div className={tournamentSearchStyles.itemAdditionalText}>
{additionalText()}
</div>
</div>
</ListBoxItem>
);

View File

@ -114,7 +114,7 @@ function RoomList({ onClose }: { onClose?: () => void }) {
>
{resolveDatePlaceholders(room.header, (d) =>
formatDateTime(d, {
month: "short",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
@ -202,7 +202,7 @@ function ChatView({ onClose }: { onClose?: () => void }) {
room?.header ?? t("common:chat.sidebar.title"),
(d) =>
formatDateTime(d, {
month: "short",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",

View File

@ -25,6 +25,7 @@ import { useUser } from "~/features/auth/core/user";
import { useChatContext } from "~/features/chat/useChatContext";
import { FriendMenu } from "~/features/friends/components/FriendMenu";
import { useHydrated } from "~/hooks/useHydrated";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import type { RootLoaderData } from "~/root";
import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
import {
@ -55,12 +56,9 @@ import { TopRightButtons } from "./TopRightButtons";
const MAX_DESKTOP_FRIENDS = 4;
function useTimeFormat() {
function useRelativeDayFormat() {
const { i18n } = useTranslation();
const formatTime = (date: Date, options: Intl.DateTimeFormatOptions) => {
return date.toLocaleTimeString(i18n.language, options);
};
const { formatTime, formatDateTime } = useTimeFormat();
const formatRelativeDay = (daysFromToday: number) => {
const rtf = new Intl.RelativeTimeFormat(i18n.language, { numeric: "auto" });
@ -70,7 +68,7 @@ function useTimeFormat() {
const formatRelativeDate = (timestamp: number) => {
const date = new Date(timestamp * 1000);
const timeStr = formatTime(date, { hour: "numeric", minute: "2-digit" });
const timeStr = formatTime(date);
if (isToday(date)) {
return `${formatRelativeDay(0)}, ${timeStr}`;
@ -79,15 +77,15 @@ function useTimeFormat() {
return `${formatRelativeDay(1)}, ${timeStr}`;
}
return date.toLocaleDateString(i18n.language, {
month: "short",
return formatDateTime(date, {
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
};
return { formatTime, formatRelativeDate };
return { formatRelativeDate };
}
function useBreadcrumbData() {
@ -215,7 +213,7 @@ export function Layout({
const setChatSidebarOpen = chatContext?.setChatOpen ?? (() => {});
const { t } = useTranslation(["front", "common"]);
const { formatRelativeDate } = useTimeFormat();
const { formatRelativeDate } = useRelativeDayFormat();
const isHydrated = useHydrated();
const location = useLocation();
const headerRef = React.useRef<HTMLElement>(null);

View File

@ -95,7 +95,7 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
<SendouDialog
heading={formatDate(databaseTimestampToDate(art.createdAt), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
})}
onClose={close}

View File

@ -23,11 +23,6 @@ export function articleBySlug(slug: string) {
return {
content,
date,
dateString: date.toLocaleDateString("en-US", {
day: "2-digit",
month: "long",
year: "numeric",
}),
authors: normalizeAuthors(restParsed.author),
title: restParsed.title,
};

View File

@ -11,7 +11,6 @@ export async function mostRecentArticles(count: number) {
const articles: Array<
Omit<NonNullable<ReturnType<typeof articleBySlug>>, "content"> & {
slug: string;
dateString: string;
}
> = [];
for (const file of files) {
@ -25,11 +24,6 @@ export async function mostRecentArticles(count: number) {
articles.push({
date,
slug: file.replace(".md", ""),
dateString: date.toLocaleDateString("en-US", {
day: "2-digit",
month: "long",
year: "numeric",
}),
authors: normalizeAuthors(restParsed.author),
title: restParsed.title,
});
@ -37,6 +31,5 @@ export async function mostRecentArticles(count: number) {
return articles
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, count)
.map(({ date: _date, ...rest }) => rest);
.slice(0, count);
}

View File

@ -3,6 +3,7 @@ import type { MetaFunction } from "react-router";
import { Link, useLoaderData } from "react-router";
import { Main } from "~/components/Main";
import { Markdown } from "~/components/Markdown";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import invariant from "~/utils/invariant";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
@ -52,12 +53,20 @@ export const meta: MetaFunction = (args) => {
export default function ArticlePage() {
const data = useLoaderData<typeof loader>();
const { formatDate } = useTimeFormat();
return (
<Main>
<article className="article">
<h1>{data.title}</h1>
<div className="text-sm text-lighter">
by <Author /> <time>{data.dateString}</time>
by <Author /> {" "}
<time>
{formatDate(new Date(data.date), {
day: "numeric",
month: "numeric",
year: "numeric",
})}
</time>
</div>
<Markdown>
{contentWithoutLeadingTitle(data.content, data.title)}

View File

@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
import type { MetaFunction } from "react-router";
import { Link, useLoaderData } from "react-router";
import { Main } from "~/components/Main";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { ARTICLES_MAIN_PAGE, articlePage, navIconUrl } from "~/utils/urls";
import { metaTags } from "../../../utils/remix";
@ -30,6 +31,7 @@ export const meta: MetaFunction = (args) => {
export default function ArticlesMainPage() {
const { t, i18n } = useTranslation(["common"]);
const { formatDate } = useTimeFormat();
const data = useLoaderData<typeof loader>();
return (
@ -46,7 +48,14 @@ export default function ArticlesMainPage() {
style: "short",
}).format(article.authors.map((a) => a.name)),
})}{" "}
<time>{article.dateString}</time>
{" "}
<time>
{formatDate(new Date(article.date), {
day: "numeric",
month: "numeric",
year: "numeric",
})}
</time>
</div>
</li>
))}

View File

@ -25,7 +25,7 @@ export default function SuspendedPage() {
<div suppressHydrationWarning>
Ends:{" "}
{formatDateTime(ends, {
month: "long",
month: "numeric",
day: "numeric",
year: "numeric",
hour: "numeric",

View File

@ -236,7 +236,7 @@ function DateFilter({
{patch} (
{formatDate(date, {
day: "numeric",
month: "long",
month: "numeric",
year: "numeric",
})}
)

View File

@ -47,7 +47,7 @@ export function TournamentCard({
}
return formatDateTimeSmartMinutes(date, {
month: "short",
month: "numeric",
day: "numeric",
hour: "numeric",
weekday: "short",

View File

@ -105,7 +105,7 @@ export default function CalendarEventPage() {
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
month: "numeric",
weekday: "long",
year: "numeric",
})

View File

@ -8,6 +8,7 @@
.navigateButtonsContainer {
display: flex;
flex-wrap: wrap;
gap: var(--s-4);
width: 100%;
}
@ -31,6 +32,9 @@
&:not(.navigateArrowButton) {
justify-content: center;
flex-basis: 100%;
background-color: var(--color-bg);
min-height: 30px;
}
& svg {
@ -50,7 +54,26 @@
}
}
.navigateArrowButtonRange {
font-variant-numeric: tabular-nums;
color: var(--color-text-high);
font-size: var(--font-2xs);
}
@container (width >= 640px) {
.navigateArrowButton {
min-width: 7.25rem;
flex-basis: auto;
}
.navigateButton:not(.navigateArrowButton) {
flex-basis: auto;
}
}
.navigateArrowButton {
gap: var(--s-1);
& svg {
margin-inline-start: -5px;
}

View File

@ -144,17 +144,16 @@ function NavigateButton({
daysInterval: ReturnType<typeof daysForCalendar>["shown"];
filters?: CalendarLoaderData["filters"];
}) {
const { formatDate } = useTimeFormat();
const { formatDateRange } = useTimeFormat();
const lowestDate = daysInterval[0];
const highestDate = daysInterval[daysInterval.length - 1];
const dateToString = (
day: ReturnType<typeof daysForCalendar>["shown"][number],
) =>
formatDate(new Date(new Date().getFullYear(), day.month, day.day), {
day: "numeric",
month: "short",
});
const year = new Date().getFullYear();
const rangeString = formatDateRange(
new Date(year, lowestDate.month, lowestDate.day),
new Date(year, highestDate.month, highestDate.day),
{ day: "numeric", month: "numeric" },
);
return (
<Link
@ -165,9 +164,7 @@ function NavigateButton({
{icon}
<div>
<div>{children}</div>
<div className="text-xxs text-lighter">
{dateToString(lowestDate)} - {dateToString(highestDate)}
</div>
<div className={styles.navigateArrowButtonRange}>{rangeString}</div>
</div>
</Link>
);
@ -264,7 +261,7 @@ function DayHeader(props: { date: number; month: number; year: number }) {
>
{formatDate(date, {
day: "numeric",
month: "long",
month: "numeric",
})}
<div className={styles.dayHeaderWeekday}>
{formatDate(date, {

View File

@ -11,6 +11,7 @@ import {
} from "~/components/elements/Menu";
import { ListButton } from "~/components/SideNav";
import { SENDOUQ_ACTIVITY_LABEL } from "~/features/friends/friends-constants";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { databaseTimestampToDate } from "~/utils/dates";
import { SENDOUQ_LOOKING_PAGE, tournamentSubsPage } from "~/utils/urls";
@ -38,12 +39,17 @@ export function FriendMenu({
onNavigate?: () => void;
}) {
const { t } = useTranslation(["common", "friends"]);
const { formatDate } = useTimeFormat();
const fetcher = useFetcher();
const [confirmOpen, setConfirmOpen] = React.useState(false);
const friendSinceText = friendshipCreatedAt
? t("friends:friendsList.friendSince", {
date: databaseTimestampToDate(friendshipCreatedAt).toLocaleDateString(),
date: formatDate(databaseTimestampToDate(friendshipCreatedAt), {
day: "numeric",
month: "numeric",
year: "numeric",
}),
})
: null;

View File

@ -71,8 +71,8 @@ function SeasonDates({
return isHydrated ? (
<div className={className}>
{formatDate(season.starts, { month: "long", day: "numeric" })} -{" "}
{formatDate(season.ends, { month: "long", day: "numeric" })}
{formatDate(season.starts, { month: "numeric", day: "numeric" })} -{" "}
{formatDate(season.ends, { month: "numeric", day: "numeric" })}
</div>
) : (
<div className={clsx(className, "invisible")}>X</div>

View File

@ -272,7 +272,7 @@ function PostTime({
return (
<div className="text-lighter text-xs font-bold">
{formatDate(createdAtDate, {
month: "long",
month: "numeric",
day: "numeric",
})}{" "}
{overDayDifferenceBetween ? (

View File

@ -1,6 +1,7 @@
import clsx from "clsx";
import type { MetaFunction } from "react-router";
import { Link, useLoaderData } from "react-router";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { metaTags, type SerializeFrom } from "~/utils/remix";
import { PLUS_SERVER_DISCORD_URL, userPage } from "~/utils/urls";
@ -21,12 +22,22 @@ export const meta: MetaFunction = (args) => {
export default function PlusVotingResultsPage() {
const data = useLoaderData<typeof loader>();
const { formatDate } = useTimeFormat();
return (
<div className="stack md">
<h2 className="text-center">
Voting results for {data.lastCompletedVoting.month + 1}/
{data.lastCompletedVoting.year}
Voting results for{" "}
{formatDate(
new Date(
data.lastCompletedVoting.year,
data.lastCompletedVoting.month,
),
{
month: "numeric",
year: "numeric",
},
)}
</h2>
{data.ownScores && data.ownScores.length > 0 ? (
<>

View File

@ -405,7 +405,7 @@ function ScrimActionButtons({
hour: "numeric",
minute: "2-digit",
day: "numeric",
month: "long",
month: "numeric",
})}
</div>
</div>

View File

@ -139,7 +139,7 @@ function ScrimHeader() {
options={{
weekday: "long",
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",

View File

@ -216,7 +216,7 @@ function ScrimsDaySection({
<h2 className="text-sm">
{formatDate(databaseTimestampToDate(posts[0].at), {
day: "numeric",
month: "long",
month: "numeric",
weekday: "long",
})}
</h2>
@ -342,7 +342,7 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) {
<h2 className="text-sm">
{formatDate(databaseTimestampToDate(posts![0].at), {
day: "numeric",
month: "long",
month: "numeric",
weekday: "long",
})}
</h2>
@ -411,7 +411,7 @@ function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) {
<h2 className="text-sm">
{formatDate(databaseTimestampToDate(posts![0].at), {
day: "numeric",
month: "long",
month: "numeric",
weekday: "long",
})}
</h2>

View File

@ -320,7 +320,7 @@ function GroupMember({
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
month: "numeric",
year: "numeric",
},
)}

View File

@ -18,6 +18,7 @@ import { useUser } from "~/features/auth/core/user";
import type * as Seasons from "~/features/mmr/core/Seasons";
import { useAutoRerender } from "~/hooks/useAutoRerender";
import { useHydrated } from "~/hooks/useHydrated";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { useHasRole } from "~/modules/permissions/hooks";
import { metaTags, type SerializeFrom } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
@ -64,6 +65,7 @@ export const meta: MetaFunction = (args) => {
export default function QPage() {
const { t } = useTranslation(["q"]);
const { formatDateTime } = useTimeFormat();
const [dialogOpen, setDialogOpen] = React.useState(true);
const user = useUser();
const data = useLoaderData<typeof loader>();
@ -132,9 +134,9 @@ export default function QPage() {
>
As a fresh account please wait before joining the queue. You
can join{" "}
{queueJoinStatus.toLocaleString("en-US", {
{formatDateTime(queueJoinStatus, {
day: "numeric",
month: "long",
month: "numeric",
hour: "numeric",
minute: "numeric",
})}
@ -184,37 +186,16 @@ const countries = [
{ id: 3, countryCode: "FR", timeZone: "Europe/Paris", city: "paris" },
{ id: 4, countryCode: "JP", timeZone: "Asia/Tokyo", city: "tokyo" },
] as const;
const weekdayFormatter = ({
timeZone,
locale,
}: {
timeZone: string;
locale: string;
}) =>
new Intl.DateTimeFormat([locale], {
timeZone,
weekday: "long",
});
const clockFormatter = ({
timeZone,
locale,
}: {
timeZone: string;
locale: string;
}) =>
new Intl.DateTimeFormat([locale], {
timeZone,
hour: "numeric",
minute: "numeric",
});
function Clocks() {
const isHydrated = useHydrated();
const { t, i18n } = useTranslation(["q"]);
const { t } = useTranslation(["q"]);
const { formatDate, formatTime } = useTimeFormat();
useAutoRerender();
return (
<div className={styles.clocksContainer}>
{countries.map((country) => {
const now = new Date();
return (
<div key={country.id} className={styles.clock}>
<div className={styles.clockCountry}>
@ -223,19 +204,20 @@ function Clocks() {
<Flag countryCode={country.countryCode} />
<div className={clsx({ invisible: !isHydrated })}>
{isHydrated
? weekdayFormatter({
? formatDate(now, {
timeZone: country.timeZone,
locale: i18n.language,
}).format(new Date())
weekday: "long",
})
: // take space
"Monday"}
</div>
<div className={clsx({ invisible: !isHydrated })}>
{isHydrated
? clockFormatter({
? formatTime(now, {
timeZone: country.timeZone,
locale: i18n.language,
}).format(new Date())
hour: "numeric",
minute: "numeric",
})
: // take space
"0:00 PM"}
</div>
@ -293,15 +275,16 @@ function ActiveSeasonInfo({
}: {
season: SerializeFrom<Seasons.ListItem>;
}) {
const { t, i18n } = useTranslation(["q"]);
const { t } = useTranslation(["q"]);
const { formatDateTime } = useTimeFormat();
const isHydrated = useHydrated();
const starts = new Date(season.starts);
const ends = new Date(season.ends);
const dateToString = (date: Date) =>
date.toLocaleString(i18n.language, {
month: "short",
formatDateTime(date, {
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
@ -401,14 +384,15 @@ function UpcomingSeasonInfo({
season: SerializeFrom<Seasons.ListItem>;
}) {
const { t } = useTranslation(["q"]);
const { formatDateTime } = useTimeFormat();
const isHydrated = useHydrated();
if (!isHydrated) return null;
const starts = new Date(season.starts);
const dateToString = (date: Date) =>
date.toLocaleString("en-US", {
month: "long",
formatDateTime(date, {
month: "numeric",
day: "numeric",
hour: "numeric",
});

View File

@ -47,7 +47,7 @@ export function TeamResultsTable({ results }: TeamResultsTableProps) {
<td className="whitespace-nowrap">
{formatDate(databaseTimestampToDate(result.startTime), {
day: "numeric",
month: "short",
month: "numeric",
year: "numeric",
})}
</td>

View File

@ -6,6 +6,10 @@ import { loader } from "../loaders/t.$customUrl.results.server";
export { loader };
export const handle = {
i18n: ["user"],
};
export default function TeamResultsPage() {
const data = useLoaderData<typeof loader>();

View File

@ -4,6 +4,7 @@ import type { MetaFunction } from "react-router";
import { useLoaderData, useSearchParams } from "react-router";
import { Main } from "~/components/Main";
import type { Tables } from "~/db/tables";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import type { RankedModeShort } from "~/modules/in-game-lists/types";
import invariant from "~/utils/invariant";
@ -37,6 +38,7 @@ export const meta: MetaFunction = (args) => {
export default function XSearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const { t } = useTranslation(["common", "game-misc"]);
const { formatDate } = useTimeFormat();
const data = useLoaderData<typeof loader>();
const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
@ -60,6 +62,12 @@ export default function XSearchPage() {
searchParams.get("mode") ?? "SZ"
}-${searchParams.get("region") ?? "WEST"}`;
const formatMonthYear = (my: MonthYear) =>
formatDate(new Date(my.year, my.month - 1), {
month: "numeric",
year: "numeric",
});
return (
<Main halfWidth className="stack lg">
<select
@ -78,8 +86,8 @@ export default function XSearchPage() {
key={option.id}
value={`${option.span.value.month}-${option.span.value.year}-${option.mode}-${option.region}`}
>
{option.span.from.month}/{option.span.from.year} -{" "}
{option.span.to.month}/{option.span.to.year} /{" "}
{formatMonthYear(option.span.from)} -{" "}
{formatMonthYear(option.span.to)} /{" "}
{t(`game-misc:MODE_SHORT_${option.mode}`)} /{" "}
{t(`common:divisions.${option.region}`)}
</option>

View File

@ -75,7 +75,7 @@ function LeagueRoundStartDate({ date }: { date: Date }) {
<div className={styles.elimRoundHeaderInfos}>
<div>
{formatDate(date, {
month: "short",
month: "numeric",
day: "numeric",
})}{" "}

View File

@ -28,6 +28,7 @@ import {
} from "~/features/tournament/routes/to.$id";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import * as PickBan from "~/features/tournament-bracket/core/PickBan";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import { modesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
@ -62,6 +63,7 @@ export function BracketMapListDialog({
isPreparing?: boolean;
}) {
const { t } = useTranslation(["common"]);
const { formatDateTime } = useTimeFormat();
const fetcher = useFetcher();
const tournament = useTournament();
const untrimmedPreparedMaps = useBracketPreparedMaps(bracketIdx);
@ -364,7 +366,13 @@ export function BracketMapListDialog({
>
Prepared by{" "}
{authorIdToUsername(tournament, preparedMaps.authorId)} @{" "}
{databaseTimestampToDate(preparedMaps.createdAt).toLocaleString()}
{formatDateTime(databaseTimestampToDate(preparedMaps.createdAt), {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
})}
</div>
) : null}
</div>

View File

@ -25,6 +25,7 @@ import {
} from "~/features/tournament/tournament-utils";
import { useHydrated } from "~/hooks/useHydrated";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import type { StageId } from "~/modules/in-game-lists/types";
import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids";
import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator/types";
@ -79,6 +80,7 @@ export function StartedMatch({
type: "EDIT" | "OTHER";
}) {
const { t } = useTranslation(["tournament"]);
const { formatDateTime } = useTimeFormat();
const isHydrated = useHydrated();
const user = useUser();
const tournament = useTournament();
@ -258,7 +260,13 @@ export function StartedMatch({
data-testid="report-timestamp"
>
{isHydrated
? databaseTimestampToDate(result.createdAt).toLocaleString()
? formatDateTime(databaseTimestampToDate(result.createdAt), {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
})
: "t"}
</div>
) : null}
@ -284,6 +292,7 @@ function FancyStageBanner({
const user = useUser();
const data = useLoaderData<TournamentMatchLoaderData>();
const { t } = useTranslation(["game-misc", "tournament"]);
const { formatDate } = useTimeFormat();
const tournament = useTournament();
const gamesCompleted = data.results.length;
@ -411,10 +420,14 @@ function FancyStageBanner({
</div>
<div>
Round playable from{" "}
{resolveLeagueRoundStartDate(
tournament,
data.match.roundId,
)!.toLocaleDateString()}{" "}
{formatDate(
resolveLeagueRoundStartDate(tournament, data.match.roundId)!,
{
day: "numeric",
month: "numeric",
year: "numeric",
},
)}{" "}
onwards
</div>
</div>

View File

@ -9,6 +9,7 @@ import { SubmitButton } from "~/components/SubmitButton";
import { useUser } from "~/features/auth/core/user";
import { soundEnabled, soundVolume } from "~/features/chat/chat-utils";
import { useTournament } from "~/features/tournament/routes/to.$id";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { logger } from "~/utils/logger";
import {
soundPath,
@ -21,6 +22,7 @@ export function TournamentTeamActions() {
const tournament = useTournament();
const user = useUser();
const fetcher = useFetcher();
const { formatTime } = useTimeFormat();
const status = tournament.teamMemberOfProgressStatus(user);
@ -104,13 +106,13 @@ export function TournamentTeamActions() {
) : bracket.startTime && bracket.startTime > new Date() ? (
<span className="text-lighter text-xxs" suppressHydrationWarning>
open{" "}
{sub(bracket.startTime, { hours: 1 }).toLocaleTimeString("en-US", {
{formatTime(sub(bracket.startTime, { hours: 1 }), {
hour: "numeric",
minute: "numeric",
weekday: "short",
})}{" "}
-{" "}
{bracket.startTime.toLocaleTimeString("en-US", {
{formatTime(bracket.startTime, {
hour: "numeric",
minute: "numeric",
})}

View File

@ -82,7 +82,7 @@ export function BannedUsersList({
<td className="text-sm text-lighter whitespace-nowrap">
{formatDate(databaseTimestampToDate(bannedUser.updatedAt), {
day: "numeric",
month: "short",
month: "numeric",
year: "numeric",
})}
</td>
@ -92,7 +92,7 @@ export function BannedUsersList({
databaseTimestampToDate(bannedUser.expiresAt),
{
day: "numeric",
month: "short",
month: "numeric",
year: "numeric",
},
)

View File

@ -132,7 +132,7 @@ function MonthSelector({ month, year }: { month: number; year: number }) {
<div>
{formatDate(date, {
year: "numeric",
month: "long",
month: "numeric",
})}
</div>
<LinkButton

View File

@ -399,7 +399,7 @@ function SeriesHeader({
<div className="text-lighter text-italic text-xs">
{t("org:events.established.short")}{" "}
{formatDate(databaseTimestampToDate(series.established), {
month: "long",
month: "numeric",
year: "numeric",
})}
</div>

View File

@ -129,7 +129,7 @@ export default function TournamentRegisterPage() {
minute: "numeric",
hour: "numeric",
day: "numeric",
month: "long",
month: "numeric",
}}
/>
) : null}

View File

@ -169,7 +169,7 @@ export function Widget({
<BigValue
value={formatDate(databaseTimestampToDate(widget.data), {
day: "numeric",
month: "short",
month: "numeric",
year: "numeric",
})}
/>
@ -417,7 +417,7 @@ function HighlightedResults({
<div className={styles.resultDate}>
{formatDate(databaseTimestampToDate(result.startTime), {
day: "numeric",
month: "short",
month: "numeric",
year: "numeric",
})}
</div>
@ -538,6 +538,7 @@ function XRankPeaks({
}
function TimezoneWidget({ timezone }: { timezone: string }) {
const { formatTime, formatDate } = useTimeFormat();
const [currentTime, setCurrentTime] = React.useState(() => new Date());
React.useEffect(() => {
@ -549,28 +550,23 @@ function TimezoneWidget({ timezone }: { timezone: string }) {
}, []);
try {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
const dateFormatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
weekday: "short",
day: "numeric",
month: "short",
});
return (
<div className="stack sm items-center">
<div className={styles.widgetValueMain} suppressHydrationWarning>
{formatter.format(currentTime)}
{formatTime(currentTime, {
timeZone: timezone,
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})}
</div>
<div className={styles.widgetValueFooter} suppressHydrationWarning>
{dateFormatter.format(currentTime)}
{formatDate(currentTime, {
timeZone: timezone,
weekday: "short",
day: "numeric",
month: "numeric",
})}
</div>
</div>
);

View File

@ -66,7 +66,7 @@ function AccountInfos() {
{data.createdAt
? formatDateTime(databaseTimestampToDate(data.createdAt), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
@ -78,7 +78,7 @@ function AccountInfos() {
<dd>
{formatDateTime(new Date(data.discordAccountCreatedAt), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
@ -131,7 +131,7 @@ function ModNotes() {
<p className="font-bold">
{formatDateTime(databaseTimestampToDate(note.createdAt), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
@ -197,7 +197,7 @@ function BanLog() {
<p className="font-bold">
{formatDateTime(databaseTimestampToDate(ban.createdAt), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
@ -215,7 +215,7 @@ function BanLog() {
{ban.banned !== 1
? formatDateTime(databaseTimestampToDate(ban.banned), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
@ -254,7 +254,7 @@ function FriendCodes() {
{index === 0 ? "Current" : "Past"} - Added on{" "}
{formatDateTime(databaseTimestampToDate(fc.createdAt), {
year: "numeric",
month: "long",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",

View File

@ -245,13 +245,13 @@ function SeasonHeader({
<>
{formatDate(new Date(starts), {
day: "numeric",
month: "long",
month: "numeric",
year: isDifferentYears ? "numeric" : undefined,
})}{" "}
-{" "}
{formatDate(new Date(ends), {
day: "numeric",
month: "long",
month: "numeric",
year: "numeric",
})}
</>
@ -777,7 +777,7 @@ function Results({
{isHydrated
? formatDate(databaseTimestampToDate(result.createdAt), {
weekday: "long",
month: "long",
month: "numeric",
day: "numeric",
})
: "t"}

View File

@ -124,6 +124,18 @@ export function useTimeFormat() {
);
};
const formatDateRange = (
from: Date,
to: Date,
options?: Intl.DateTimeFormatOptions,
) => {
const adjusted = withYearFirstAdjustment(options, dateFormat);
const locale = isNumericMonth(adjusted) ? dateLocale : i18n.language;
return new Intl.DateTimeFormat(locale, adjusted)
.formatRange(from, to)
.replace(/\s*\s*/g, " ");
};
/** Same as `formatDateTime` but omits minutes when they are zero and AM/PM format is in use */
const formatDateTimeSmartMinutes = (
date: Date,
@ -171,6 +183,7 @@ export function useTimeFormat() {
formatDateTime,
formatTime,
formatDate,
formatDateRange,
formatDateTimeSmartMinutes,
formatDistanceToNow,
formatDuration,