diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx index fe38bb391..6d36aeae2 100644 --- a/app/components/BuildCard.tsx +++ b/app/components/BuildCard.tsx @@ -5,6 +5,7 @@ import type { GearType, Tables, UserWithPlusTier } from "~/db/tables"; import { useUser } from "~/features/auth/core/user"; import type { BuildWeaponWithTop500Info } from "~/features/builds/builds-types"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import type { Ability as AbilityType, BuildAbilitiesTuple, @@ -57,7 +58,7 @@ interface BuildProps { export function BuildCard({ build, owner, canEdit = false }: BuildProps) { const user = useUser(); const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]); - const { i18n } = useTranslation(); + const { formatDate } = useTimeFormat(); const isMounted = useIsMounted(); const { @@ -129,14 +130,11 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) { className={clsx("whitespace-nowrap", { invisible: !isMounted })} > {isMounted - ? databaseTimestampToDate(updatedAt).toLocaleDateString( - i18n.language, - { - day: "numeric", - month: "long", - year: "numeric", - }, - ) + ? formatDate(databaseTimestampToDate(updatedAt), { + day: "numeric", + month: "long", + year: "numeric", + }) : "t"} diff --git a/app/components/Chart.tsx b/app/components/Chart.tsx index 76e3f5504..d3fe5d998 100644 --- a/app/components/Chart.tsx +++ b/app/components/Chart.tsx @@ -5,6 +5,7 @@ import type { TooltipRendererProps } from "react-charts/types/components/Tooltip import { useTranslation } from "react-i18next"; import { Theme, useTheme } from "~/features/theme/core/provider"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; export default function Chart({ options, @@ -104,7 +105,7 @@ function ChartTooltip({ headerSuffix = "", valueSuffix = "", }: ChartTooltipProps) { - const { i18n } = useTranslation(); + const { formatDate } = useTimeFormat(); const dataPoints = focusedDatum?.interactiveGroup ?? []; const header = () => { @@ -112,7 +113,7 @@ function ChartTooltip({ if (!primaryValue) return null; if (primaryValue instanceof Date) { - return primaryValue.toLocaleDateString(i18n.language, { + return formatDate(primaryValue, { weekday: "short", day: "numeric", month: "long", diff --git a/app/components/RelativeTime.tsx b/app/components/RelativeTime.tsx index 7c9fa3e75..4d89a40bf 100644 --- a/app/components/RelativeTime.tsx +++ b/app/components/RelativeTime.tsx @@ -1,5 +1,6 @@ import type * as React from "react"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; export function RelativeTime({ children, @@ -9,12 +10,13 @@ export function RelativeTime({ timestamp: number; }) { const isMounted = useIsMounted(); + const { formatDateTime } = useTimeFormat(); return ( - {time.toLocaleString(i18n.language, options)} + {formatDateTime(time, options)}
- {time.toLocaleTimeString(i18n.language, { + {formatTime(time, { timeZoneName: "long", hour: "numeric", minute: "numeric", diff --git a/app/components/elements/Toast.module.css b/app/components/elements/Toast.module.css index b1ce7d186..84929ffc3 100644 --- a/app/components/elements/Toast.module.css +++ b/app/components/elements/Toast.module.css @@ -3,7 +3,7 @@ gap: 8px; display: flex; position: fixed; - top: 10px; + top: 55px; right: 10px; z-index: 10; } diff --git a/app/db/tables.ts b/app/db/tables.ts index fc0af6686..98df43a80 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -832,6 +832,14 @@ export interface UserPreferences { disallowScrimPickupsFromUntrusted?: boolean; defaultCalendarFilters?: CalendarFilters; defaultScrimsFilters?: ScrimFilters; + /** + * What time format the user prefers? + * + * "auto" = use browser default (default value) + * "24h" = 24 hour format (e.g. 14:00) + * "12h" = 12 hour format (e.g. 2:00 PM) + * */ + clockFormat?: "24h" | "12h" | "auto"; } export interface User { diff --git a/app/features/art/components/ArtGrid.tsx b/app/features/art/components/ArtGrid.tsx index 642df5009..aaf9eebe5 100644 --- a/app/features/art/components/ArtGrid.tsx +++ b/app/features/art/components/ArtGrid.tsx @@ -15,6 +15,7 @@ import { Pagination } from "~/components/Pagination"; import { useIsMounted } from "~/hooks/useIsMounted"; import { usePagination } from "~/hooks/usePagination"; import { useSearchParamState } from "~/hooks/useSearchParamState"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { artPage, newArtPage, userArtPage, userPage } from "~/utils/urls"; import { ResponsiveMasonry } from "../../../modules/responsive-masonry/components/ResponsiveMasonry"; @@ -90,19 +91,16 @@ export function ArtGrid({ } function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) { - const { i18n } = useTranslation(); const [imageLoaded, setImageLoaded] = React.useState(false); + const { formatDate } = useTimeFormat(); return ( diff --git a/app/features/ban/routes/suspended.tsx b/app/features/ban/routes/suspended.tsx index 21b826d84..921ffd99c 100644 --- a/app/features/ban/routes/suspended.tsx +++ b/app/features/ban/routes/suspended.tsx @@ -1,5 +1,6 @@ import { useLoaderData } from "@remix-run/react"; import { Main } from "~/components/Main"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { loader } from "../loaders/suspended.server"; @@ -7,6 +8,7 @@ export { loader }; export default function SuspendedPage() { const data = useLoaderData(); + const { formatDateTime } = useTimeFormat(); const ends = (() => { if (!data.banned || data.banned === 1) return null; @@ -21,7 +23,7 @@ export default function SuspendedPage() { {ends ? (
Ends:{" "} - {ends.toLocaleString("en-US", { + {formatDateTime(ends, { month: "long", day: "numeric", year: "numeric", diff --git a/app/features/builds/components/FilterSection.tsx b/app/features/builds/components/FilterSection.tsx index 28e888625..8a5838f16 100644 --- a/app/features/builds/components/FilterSection.tsx +++ b/app/features/builds/components/FilterSection.tsx @@ -5,6 +5,7 @@ import { SendouButton } from "~/components/elements/Button"; import { ModeImage } from "~/components/Image"; import { CrossIcon } from "~/components/icons/Cross"; import { possibleApValues } from "~/features/build-analyzer"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { abilities } from "~/modules/in-game-lists/abilities"; import { modesShort } from "~/modules/in-game-lists/modes"; import type { @@ -194,7 +195,8 @@ function DateFilter({ filter: DateBuildFilter; onChange: (filter: Partial) => void; }) { - const { t, i18n } = useTranslation(["builds"]); + const { t } = useTranslation(["builds"]); + const { formatDate } = useTimeFormat(); const selectValue = () => { const dateString = dateToYYYYMMDD(new Date(filter.date)); @@ -239,7 +241,7 @@ function DateFilter({ return (
{isMounted ? (
- {season.starts.toLocaleDateString(i18n.language, { + {formatDate(season.starts, { month: "long", day: "numeric", })}{" "} -{" "} - {season.ends.toLocaleDateString(i18n.language, { + {formatDate(season.ends, { month: "long", day: "numeric", })} diff --git a/app/features/lfg/components/LFGPost.tsx b/app/features/lfg/components/LFGPost.tsx index e227152f7..ab4bce79e 100644 --- a/app/features/lfg/components/LFGPost.tsx +++ b/app/features/lfg/components/LFGPost.tsx @@ -15,6 +15,7 @@ import { useUser } from "~/features/auth/core/user"; import * as Seasons from "~/features/mmr/core/Seasons"; import type { TieredSkill } from "~/features/mmr/tiered.server"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { useHasRole } from "~/modules/permissions/hooks"; import { databaseTimestampToDate } from "~/utils/dates"; import { lfgNewPostPage, navIconUrl, userPage } from "~/utils/urls"; @@ -258,7 +259,8 @@ function PostTime({ createdAt: number; updatedAt: number; }) { - const { t, i18n } = useTranslation(["lfg"]); + const { t } = useTranslation(["lfg"]); + const { formatDate } = useTimeFormat(); const createdAtDate = databaseTimestampToDate(createdAt); const updatedAtDate = databaseTimestampToDate(updatedAt); @@ -267,7 +269,7 @@ function PostTime({ return (
- {createdAtDate.toLocaleString(i18n.language, { + {formatDate(createdAtDate, { month: "long", day: "numeric", })}{" "} diff --git a/app/features/scrims/components/ScrimCard.tsx b/app/features/scrims/components/ScrimCard.tsx index 8a341d9f6..5f2d28d0d 100644 --- a/app/features/scrims/components/ScrimCard.tsx +++ b/app/features/scrims/components/ScrimCard.tsx @@ -19,6 +19,7 @@ import { TrashIcon } from "~/components/icons/Trash"; import { UsersIcon } from "~/components/icons/Users"; import TimePopover from "~/components/TimePopover"; import { useUser } from "~/features/auth/core/user"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import type { ModeShort } from "~/modules/in-game-lists/types"; import { databaseTimestampToDate } from "~/utils/dates"; import { scrimPage, tournamentRegisterPage, userPage } from "~/utils/urls"; @@ -333,7 +334,8 @@ function ScrimActionButtons({ action: ScrimPostCardProps["action"]; post: ScrimPost; }) { - const { t, i18n } = useTranslation(["scrims", "common"]); + const { t } = useTranslation(["scrims", "common"]); + const { formatDateTime } = useTimeFormat(); const user = useUser(); const [isRequestModalOpen, setIsRequestModalOpen] = useState(false); const [isViewRequestModalOpen, setIsViewRequestModalOpen] = useState(false); @@ -399,15 +401,12 @@ function ScrimActionButtons({ {t("scrims:requestModal.at.label")}
- {databaseTimestampToDate(userRequest.at).toLocaleString( - i18n.language, - { - hour: "numeric", - minute: "2-digit", - day: "numeric", - month: "long", - }, - )} + {formatDateTime(databaseTimestampToDate(userRequest.at), { + hour: "numeric", + minute: "2-digit", + day: "numeric", + month: "long", + })}
) : null} @@ -474,7 +473,8 @@ export function ScrimRequestCard({ canAccept, showFooter = true, }: ScrimRequestCardProps) { - const { t, i18n } = useTranslation(["scrims", "common"]); + const { t } = useTranslation(["scrims", "common"]); + const { formatTime } = useTimeFormat(); const owner = request.users.find((user) => user.isOwner) ?? request.users[0]; const isPickup = !request.team?.name; @@ -533,7 +533,7 @@ export function ScrimRequestCard({ data-testid="confirm-modal-trigger-button" > {t("scrims:acceptModal.confirmFor", { - time: confirmedTime.toLocaleTimeString(i18n.language, { + time: formatTime(confirmedTime, { hour: "numeric", minute: "2-digit", }), @@ -545,7 +545,7 @@ export function ScrimRequestCard({ trigger={ {t("scrims:acceptModal.confirmFor", { - time: confirmedTime.toLocaleTimeString(i18n.language, { + time: formatTime(confirmedTime, { hour: "numeric", minute: "2-digit", }), diff --git a/app/features/scrims/components/ScrimRequestModal.tsx b/app/features/scrims/components/ScrimRequestModal.tsx index b651fbaea..d6feb122d 100644 --- a/app/features/scrims/components/ScrimRequestModal.tsx +++ b/app/features/scrims/components/ScrimRequestModal.tsx @@ -5,6 +5,7 @@ import { SendouDialog } from "~/components/elements/Dialog"; import { SelectFormField } from "~/components/form/SelectFormField"; import { SendouForm } from "~/components/form/SendouForm"; import { TextAreaFormField } from "~/components/form/TextAreaFormField"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { joinListToNaturalString, nullFilledArray } from "~/utils/arrays"; import { databaseTimestampToDate } from "~/utils/dates"; import type { loader as scrimsLoader } from "../loaders/scrims.server"; @@ -22,8 +23,9 @@ export function ScrimRequestModal({ post: ScrimPost; close: () => void; }) { - const { t, i18n } = useTranslation(["scrims"]); + const { t } = useTranslation(["scrims"]); const data = useLoaderData(); + const { formatTime } = useTimeFormat(); const timeOptions = post.rangeEnd ? generateTimeOptions( @@ -31,7 +33,7 @@ export function ScrimRequestModal({ databaseTimestampToDate(post.rangeEnd), ).map((timestamp) => ({ value: timestamp, - label: new Date(timestamp).toLocaleTimeString(i18n.language, { + label: formatTime(new Date(timestamp), { hour: "numeric", minute: "2-digit", }), diff --git a/app/features/scrims/routes/scrims.tsx b/app/features/scrims/routes/scrims.tsx index 0ae173f59..29d86a38d 100644 --- a/app/features/scrims/routes/scrims.tsx +++ b/app/features/scrims/routes/scrims.tsx @@ -9,6 +9,7 @@ import { AddNewButton } from "~/components/AddNewButton"; import { LinkButton, SendouButton } from "~/components/elements/Button"; import { useUser } from "~/features/auth/core/user"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; @@ -201,10 +202,10 @@ function ScrimsDaySection({ posts: ScrimPost[]; filters: ScrimFilters; }) { - const { i18n } = useTranslation(); const user = useUser(); const [showFiltered, setShowFiltered] = React.useState(false); const [showRequestPending, setShowRequestPending] = React.useState(false); + const { formatDate } = useTimeFormat(); const filteredPosts = posts.filter((post) => Scrim.applyFilters(post, filters), @@ -220,14 +221,11 @@ function ScrimsDaySection({

- {databaseTimestampToDate(posts[0].at).toLocaleDateString( - i18n.language, - { - day: "numeric", - month: "long", - weekday: "long", - }, - )} + {formatDate(databaseTimestampToDate(posts[0].at), { + day: "numeric", + month: "long", + weekday: "long", + })}

{user ? ( databaseTimestampToDate(post.at).getDate(), @@ -348,14 +347,11 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) { return (

- {databaseTimestampToDate(posts![0].at).toLocaleDateString( - i18n.language, - { - day: "numeric", - month: "long", - weekday: "long", - }, - )} + {formatDate(databaseTimestampToDate(posts![0].at), { + day: "numeric", + month: "long", + weekday: "long", + })}

{posts!.map((post) => { @@ -406,7 +402,7 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) { } function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) { - const { i18n } = useTranslation(); + const { formatDate } = useTimeFormat(); const postsByDay = R.groupBy(posts, (post) => databaseTimestampToDate(post.at).getDate(), @@ -420,14 +416,11 @@ function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) { return (

- {databaseTimestampToDate(posts![0].at).toLocaleDateString( - i18n.language, - { - day: "numeric", - month: "long", - weekday: "long", - }, - )} + {formatDate(databaseTimestampToDate(posts![0].at), { + day: "numeric", + month: "long", + weekday: "long", + })}

{posts!.map((post) => { diff --git a/app/features/sendouq-match/routes/q.match.$id.tsx b/app/features/sendouq-match/routes/q.match.$id.tsx index cf25868a9..7af64af59 100644 --- a/app/features/sendouq-match/routes/q.match.$id.tsx +++ b/app/features/sendouq-match/routes/q.match.$id.tsx @@ -45,6 +45,7 @@ import { AddPrivateNoteDialog } from "~/features/sendouq-match/components/AddPri import type { ReportedWeaponForMerging } from "~/features/sendouq-match/core/reported-weapons.server"; import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { useWindowSize } from "~/hooks/useWindowSize"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids"; @@ -105,7 +106,8 @@ export default function QMatchPage() { const user = useUser(); const isStaff = useHasRole("STAFF"); const isMounted = useIsMounted(); - const { t, i18n } = useTranslation(["q"]); + const { t } = useTranslation(["q"]); + const { formatDateTime } = useTimeFormat(); const data = useLoaderData(); const [showWeaponsForm, setShowWeaponsForm] = React.useState(false); const [searchParams] = useSearchParams(); @@ -154,16 +156,13 @@ export default function QMatchPage() { })} > {isMounted - ? databaseTimestampToDate(data.match.createdAt).toLocaleString( - i18n.language, - { - day: "numeric", - month: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - }, - ) + ? formatDateTime(databaseTimestampToDate(data.match.createdAt), { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + }) : // reserve place "0/0/0 0:00"}
@@ -243,7 +242,8 @@ function Score({ ownTeamReported: boolean; }) { const isMounted = useIsMounted(); - const { t, i18n } = useTranslation(["q"]); + const { t } = useTranslation(["q"]); + const { formatDateTime } = useTimeFormat(); const data = useLoaderData(); const reporter = data.groupAlpha.members.find((m) => m.id === data.match.reportedByUserId) ?? @@ -292,16 +292,13 @@ function Score({ > {t("q:match.reportedBy", { name: reporter?.username ?? "admin" })}{" "} {isMounted - ? databaseTimestampToDate(reportedAt).toLocaleString( - i18n.language, - { - day: "numeric", - month: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - }, - ) + ? formatDateTime(databaseTimestampToDate(reportedAt), { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + }) : ""}
) : ( diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx index 1f737d04d..d25f78d51 100644 --- a/app/features/sendouq/components/GroupCard.tsx +++ b/app/features/sendouq/components/GroupCard.tsx @@ -22,6 +22,7 @@ import { useUser } from "~/features/auth/core/user"; import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "~/features/leaderboards/leaderboards-constants"; import { ordinalToRoundedSp } from "~/features/mmr/mmr-utils"; import type { TieredSkill } from "~/features/mmr/tiered.server"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { languagesUnified } from "~/modules/i18n/config"; import type { ModeShort } from "~/modules/in-game-lists/types"; import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids"; @@ -253,8 +254,9 @@ function GroupMember({ showAddNote?: SqlBool; showNote?: boolean; }) { - const { t, i18n } = useTranslation(["q", "user"]); + const { t } = useTranslation(["q", "user"]); const user = useUser(); + const { formatDateTime } = useTimeFormat(); return (
@@ -283,15 +285,16 @@ function GroupMember({ )} >
- {databaseTimestampToDate( - member.privateNote.updatedAt, - ).toLocaleString(i18n.language, { - hour: "numeric", - minute: "numeric", - day: "numeric", - month: "long", - year: "numeric", - })} + {formatDateTime( + databaseTimestampToDate(member.privateNote.updatedAt), + { + hour: "numeric", + minute: "numeric", + day: "numeric", + month: "long", + year: "numeric", + }, + )}
(); const fetcher = useFetcher(); + const { formatTime } = useTimeFormat(); if (data.expiryStatus === "EXPIRED") { return ( @@ -161,13 +163,10 @@ function InfoText() { {isMounted ? t("q:looking.lastUpdatedAt", { - time: new Date(data.lastUpdated).toLocaleTimeString( - i18n.language, - { - hour: "2-digit", - minute: "2-digit", - }, - ), + time: formatTime(new Date(data.lastUpdated), { + hour: "2-digit", + minute: "2-digit", + }), }) : "Placeholder"} diff --git a/app/features/settings/actions/settings.server.ts b/app/features/settings/actions/settings.server.ts index dd2843a05..cf945b7cc 100644 --- a/app/features/settings/actions/settings.server.ts +++ b/app/features/settings/actions/settings.server.ts @@ -2,7 +2,7 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { requireUser } from "~/features/auth/core/user.server"; import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { parseRequestPayload } from "~/utils/remix.server"; +import { parseRequestPayload, successToast } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { settingsEditSchema } from "../settings-schemas"; @@ -33,7 +33,10 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); break; } - case "PLACEHOLDER": { + case "UPDATE_CLOCK_FORMAT": { + await UserRepository.updatePreferences(user.id, { + clockFormat: data.newValue, + }); break; } default: { @@ -41,5 +44,5 @@ export const action = async ({ request }: ActionFunctionArgs) => { } } - return null; + return successToast("Settings updated"); }; diff --git a/app/features/settings/routes/settings.tsx b/app/features/settings/routes/settings.tsx index 891ed93ae..de0060c8d 100644 --- a/app/features/settings/routes/settings.tsx +++ b/app/features/settings/routes/settings.tsx @@ -42,6 +42,7 @@ export default function SettingsPage() {

{t("common:pages.settings")}

+ {user ? : null} {user ? ( <> @@ -157,6 +158,38 @@ function ThemeSelector() { ); } +function ClockFormatSelector() { + const { t } = useTranslation(["common"]); + const user = useUser(); + const fetcher = useFetcher(); + + const handleClockFormatChange = ( + event: React.ChangeEvent, + ) => { + const newFormat = event.target.value as "auto" | "24h" | "12h"; + fetcher.submit( + { _action: "UPDATE_CLOCK_FORMAT", newValue: newFormat }, + { method: "post", encType: "application/json" }, + ); + }; + + return ( +
+ + +
+ ); +} + // adapted from https://pqvst.com/2023/11/21/web-push-notifications/ function PushNotificationsEnabler() { const { t } = useTranslation(["common"]); diff --git a/app/features/settings/settings-schemas.ts b/app/features/settings/settings-schemas.ts index 22ee93a1e..b72102877 100644 --- a/app/features/settings/settings-schemas.ts +++ b/app/features/settings/settings-schemas.ts @@ -15,6 +15,7 @@ export const settingsEditSchema = z.union([ newValue: z.boolean(), }), z.object({ - _action: _action("PLACEHOLDER"), + _action: _action("UPDATE_CLOCK_FORMAT"), + newValue: z.enum(["auto", "24h", "12h"]), }), ]); diff --git a/app/features/team/components/TeamResultsTable.tsx b/app/features/team/components/TeamResultsTable.tsx index 39c616bd5..6af487124 100644 --- a/app/features/team/components/TeamResultsTable.tsx +++ b/app/features/team/components/TeamResultsTable.tsx @@ -7,6 +7,7 @@ import { UsersIcon } from "~/components/icons/Users"; import { Placement } from "~/components/Placement"; import { Table } from "~/components/Table"; import type { TeamResultsLoaderData } from "~/features/team/loaders/t.$customUrl.results.server"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { tournamentTeamPage, userPage } from "~/utils/urls"; @@ -17,7 +18,8 @@ interface TeamResultsTableProps { } export function TeamResultsTable({ results }: TeamResultsTableProps) { - const { t, i18n } = useTranslation("user"); + const { t } = useTranslation("user"); + const { formatDate } = useTimeFormat(); return ( @@ -42,14 +44,11 @@ export function TeamResultsTable({ results }: TeamResultsTableProps) {
- {databaseTimestampToDate(result.startTime).toLocaleDateString( - i18n.language, - { - day: "numeric", - month: "short", - year: "numeric", - }, - )} + {formatDate(databaseTimestampToDate(result.startTime), { + day: "numeric", + month: "short", + year: "numeric", + })}
diff --git a/app/features/tournament-bracket/components/Bracket/RoundHeader.tsx b/app/features/tournament-bracket/components/Bracket/RoundHeader.tsx index 4b823f600..b0282d449 100644 --- a/app/features/tournament-bracket/components/Bracket/RoundHeader.tsx +++ b/app/features/tournament-bracket/components/Bracket/RoundHeader.tsx @@ -4,6 +4,7 @@ import { useTournament } from "~/features/tournament/routes/to.$id"; import { resolveLeagueRoundStartDate } from "~/features/tournament/tournament-utils"; import { useAutoRerender } from "~/hooks/useAutoRerender"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { TOURNAMENT } from "../../../tournament/tournament-constants"; import { useDeadline } from "./useDeadline"; @@ -51,15 +52,7 @@ export function RoundHeader({ {hasDeadline ? : null}
) : leagueRoundStartDate ? ( -
-
- {leagueRoundStartDate.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - })}{" "} - → -
-
+ ) : (
Hidden @@ -69,10 +62,27 @@ export function RoundHeader({ ); } +function LeagueRoundStartDate({ date }: { date: Date }) { + const { formatDate } = useTimeFormat(); + + return ( +
+
+ {formatDate(date, { + month: "short", + day: "numeric", + })}{" "} + → +
+
+ ); +} + function Deadline({ roundId, bestOf }: { roundId: number; bestOf: number }) { useAutoRerender("ten seconds"); const isMounted = useIsMounted(); const deadline = useDeadline(roundId, bestOf); + const { formatTime } = useTimeFormat(); if (!deadline) return null; @@ -83,7 +93,7 @@ function Deadline({ roundId, bestOf }: { roundId: number; bestOf: number }) { })} > DL{" "} - {deadline.toLocaleTimeString("en-US", { + {formatTime(deadline, { hour: "numeric", minute: "numeric", })} diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.tsx index 0658a4a32..b558c9473 100644 --- a/app/features/tournament-bracket/routes/to.$id.brackets.tsx +++ b/app/features/tournament-bracket/routes/to.$id.brackets.tsx @@ -20,6 +20,7 @@ import { TOURNAMENT } from "~/features/tournament/tournament-constants"; import { tournamentWebsocketRoom } from "~/features/tournament-bracket/tournament-bracket-utils"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useSearchParamState } from "~/hooks/useSearchParamState"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { useVisibilityChange } from "~/hooks/useVisibilityChange"; import { SENDOU_INK_BASE_URL, tournamentJoinPage } from "~/utils/urls"; import { @@ -40,6 +41,7 @@ import "../components/Bracket/bracket.css"; export default function TournamentBracketsPage() { const { t } = useTranslation(["tournament"]); + const { formatDateTime, formatTime } = useTimeFormat(); const visibility = useVisibilityChange(); const { revalidate } = useRevalidator(); const user = useUser(); @@ -253,16 +255,13 @@ export default function TournamentBracketsPage() { {bracket.startTime ? ( (open{" "} - {sub(bracket.startTime, { hours: 1 }).toLocaleString( - "en-US", - { - hour: "numeric", - minute: "numeric", - weekday: "long", - }, - )}{" "} + {formatDateTime(sub(bracket.startTime, { hours: 1 }), { + hour: "numeric", + minute: "numeric", + weekday: "long", + })}{" "} -{" "} - {bracket.startTime.toLocaleTimeString("en-US", { + {formatTime(bracket.startTime, { hour: "numeric", minute: "numeric", })} diff --git a/app/features/tournament-organization/components/BannedPlayersList.tsx b/app/features/tournament-organization/components/BannedPlayersList.tsx index 91719fc53..e5424234a 100644 --- a/app/features/tournament-organization/components/BannedPlayersList.tsx +++ b/app/features/tournament-organization/components/BannedPlayersList.tsx @@ -9,6 +9,7 @@ import { FormWithConfirm } from "~/components/FormWithConfirm"; import { Table } from "~/components/Table"; import { BanUserModal } from "~/features/tournament-organization/components/BanUserModal"; import type { OrganizationPageLoaderData } from "~/features/tournament-organization/loaders/org.$slug.server"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { userPage } from "~/utils/urls"; import styles from "../components/BannedPlayersList.module.css"; @@ -20,7 +21,8 @@ export function BannedUsersList({ }: { bannedUsers: NonNullable; }) { - const { t, i18n } = useTranslation(["org"]); + const { t } = useTranslation(["org"]); + const { formatDate } = useTimeFormat(); const bannedUsersKey = (bannedUsers ?? []) .map((u) => [u.id, u.privateNote].join("-")) @@ -78,9 +80,7 @@ export function BannedUsersList({
- {databaseTimestampToDate( - bannedUser.updatedAt, - ).toLocaleDateString(i18n.language, { + {formatDate(databaseTimestampToDate(bannedUser.updatedAt), { day: "numeric", month: "short", year: "numeric", @@ -88,13 +88,14 @@ export function BannedUsersList({ {bannedUser.expiresAt - ? databaseTimestampToDate( - bannedUser.expiresAt, - ).toLocaleDateString(i18n.language, { - day: "numeric", - month: "short", - year: "numeric", - }) + ? formatDate( + databaseTimestampToDate(bannedUser.expiresAt), + { + day: "numeric", + month: "short", + year: "numeric", + }, + ) : t("org:banned.permanent")} diff --git a/app/features/tournament-organization/components/EventCalendar.tsx b/app/features/tournament-organization/components/EventCalendar.tsx index 7e4914546..c7c26341e 100644 --- a/app/features/tournament-organization/components/EventCalendar.tsx +++ b/app/features/tournament-organization/components/EventCalendar.tsx @@ -3,6 +3,7 @@ import clsx from "clsx"; import { LinkButton } from "~/components/elements/Button"; import type { MonthYear } from "~/features/plus-voting/core"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate, nullPaddedDatesOfMonth } from "~/utils/dates"; import type { loader } from "../loaders/org.$slug.server"; @@ -105,6 +106,7 @@ const monthYearSearchParams = ({ month, year }: MonthYear) => ]).toString(); function MonthSelector({ month, year }: { month: number; year: number }) { const date = new Date(Date.UTC(year, month, 15)); + const { formatDate } = useTimeFormat(); return (
@@ -123,7 +125,7 @@ function MonthSelector({ month, year }: { month: number; year: number }) { {"<"}
- {date.toLocaleDateString("en-US", { + {formatDate(date, { year: "numeric", month: "long", })} diff --git a/app/features/tournament-organization/routes/org.$slug.tsx b/app/features/tournament-organization/routes/org.$slug.tsx index db7528f3c..3f8bc94c1 100644 --- a/app/features/tournament-organization/routes/org.$slug.tsx +++ b/app/features/tournament-organization/routes/org.$slug.tsx @@ -26,6 +26,7 @@ import { Pagination } from "~/components/Pagination"; import { Placement } from "~/components/Placement"; import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay"; import { BannedUsersList } from "~/features/tournament-organization/components/BannedPlayersList"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { useHasPermission, useHasRole } from "~/modules/permissions/hooks"; import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates"; import { metaTags } from "~/utils/remix"; @@ -324,7 +325,8 @@ function SeriesHeader({ }: { series: NonNullable["series"]>; }) { - const { i18n, t } = useTranslation(["org"]); + const { t } = useTranslation(["org"]); + const { formatDate } = useTimeFormat(); return (
@@ -343,13 +345,10 @@ function SeriesHeader({ {series.established ? (
{t("org:events.established.short")}{" "} - {databaseTimestampToDate(series.established).toLocaleDateString( - i18n.language, - { - month: "long", - year: "numeric", - }, - )} + {formatDate(databaseTimestampToDate(series.established), { + month: "long", + year: "numeric", + })}
) : null}
@@ -450,7 +449,7 @@ function EventInfo({ event: SerializeFrom["events"][number]; showYear?: boolean; }) { - const { i18n } = useTranslation(); + const { formatDateTime } = useTimeFormat(); return (
@@ -468,16 +467,13 @@ function EventInfo({
{event.name}
diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index db034d560..e4e640eb8 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -40,6 +40,7 @@ import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tour import { useAutoRerender } from "~/hooks/useAutoRerender"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useSearchParamState } from "~/hooks/useSearchParamState"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes"; import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; @@ -376,9 +377,10 @@ function RegistrationProgress({ members?: unknown[]; mapPool?: unknown[]; }) { - const { i18n, t } = useTranslation(["tournament"]); + const { t } = useTranslation(["tournament"]); const tournament = useTournament(); const isMounted = useIsMounted(); + const { formatTime } = useTimeFormat(); const completedIfTruthy = (condition: unknown) => condition ? "completed" : "incomplete"; @@ -419,15 +421,17 @@ function RegistrationProgress({ tournament.ctx.startTime.getTime(); const registrationClosesAtString = isMounted - ? (tournament.isLeagueSignup - ? tournament.ctx.startTime - : tournament.registrationClosesAt - ).toLocaleTimeString(i18n.language, { - minute: "numeric", - hour: "numeric", - day: "2-digit", - month: "2-digit", - }) + ? formatTime( + tournament.isLeagueSignup + ? tournament.ctx.startTime + : tournament.registrationClosesAt, + { + minute: "numeric", + hour: "numeric", + day: "2-digit", + month: "2-digit", + }, + ) : ""; return ( @@ -502,14 +506,15 @@ function CheckIn({ endDate: Date; checkedIn?: boolean; }) { - const { t, i18n } = useTranslation(["tournament"]); + const { t } = useTranslation(["tournament"]); const isMounted = useIsMounted(); const fetcher = useFetcher(); + const { formatTime } = useTimeFormat(); useAutoRerender(); const checkInStartsString = isMounted - ? startDate.toLocaleTimeString(i18n.language, { + ? formatTime(startDate, { minute: "numeric", hour: "numeric", day: "2-digit", @@ -518,7 +523,7 @@ function CheckIn({ : ""; const checkInEndsString = isMounted - ? endDate.toLocaleTimeString(i18n.language, { + ? formatTime(endDate, { minute: "numeric", hour: "numeric", day: "2-digit", diff --git a/app/features/user-page/components/UserResultsTable.tsx b/app/features/user-page/components/UserResultsTable.tsx index da8180aa3..23359cce9 100644 --- a/app/features/user-page/components/UserResultsTable.tsx +++ b/app/features/user-page/components/UserResultsTable.tsx @@ -6,6 +6,7 @@ import { SendouPopover } from "~/components/elements/Popover"; import { UsersIcon } from "~/components/icons/Users"; import { Placement } from "~/components/Placement"; import { Table } from "~/components/Table"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { calendarEventPage, @@ -30,7 +31,8 @@ export function UserResultsTable({ id, hasHighlightCheckboxes, }: UserResultsTableProps) { - const { t, i18n } = useTranslation("user"); + const { t } = useTranslation("user"); + const { formatDate } = useTimeFormat(); const placementHeaderId = `${id}-th-placement`; @@ -82,14 +84,11 @@ export function UserResultsTable({
- {databaseTimestampToDate(result.startTime).toLocaleDateString( - i18n.language, - { - day: "numeric", - month: "short", - year: "numeric", - }, - )} + {formatDate(databaseTimestampToDate(result.startTime), { + day: "numeric", + month: "short", + year: "numeric", + })}
diff --git a/app/features/user-page/routes/u.$identifier.admin.tsx b/app/features/user-page/routes/u.$identifier.admin.tsx index 2f6b6d008..ab9c2d364 100644 --- a/app/features/user-page/routes/u.$identifier.admin.tsx +++ b/app/features/user-page/routes/u.$identifier.admin.tsx @@ -11,6 +11,7 @@ import { Main } from "~/components/Main"; import { useUser } from "~/features/auth/core/user"; import { USER } from "~/features/user-page/user-page-constants"; import { addModNoteSchema } from "~/features/user-page/user-page-schemas"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { action } from "../actions/u.$identifier.admin.server"; import { loader } from "../loaders/u.$identifier.admin.server"; @@ -48,13 +49,14 @@ export default function UserAdminPage() { function AccountInfos() { const data = useLoaderData(); + const { formatDateTime } = useTimeFormat(); return (
User account created at
{data.createdAt - ? databaseTimestampToDate(data.createdAt).toLocaleString("en-US", { + ? formatDateTime(databaseTimestampToDate(data.createdAt), { year: "numeric", month: "long", day: "numeric", @@ -66,7 +68,7 @@ function AccountInfos() {
Discord account created at
- {new Date(data.discordAccountCreatedAt).toLocaleString("en-US", { + {formatDateTime(new Date(data.discordAccountCreatedAt), { year: "numeric", month: "long", day: "numeric", @@ -103,6 +105,7 @@ function AccountInfos() { function ModNotes() { const user = useUser(); const data = useLoaderData(); + const { formatDateTime } = useTimeFormat(); if (!data.modNotes || data.modNotes.length === 0) { return ( @@ -118,7 +121,7 @@ function ModNotes() { {data.modNotes.map((note) => (

- {databaseTimestampToDate(note.createdAt).toLocaleString("en-US", { + {formatDateTime(databaseTimestampToDate(note.createdAt), { year: "numeric", month: "long", day: "numeric", @@ -186,6 +189,7 @@ function NewModNoteDialog() { function BanLog() { const data = useLoaderData(); + const { formatDateTime } = useTimeFormat(); if (!data.banLogs || data.banLogs.length === 0) { return

No bans

; @@ -196,7 +200,7 @@ function BanLog() { {data.banLogs.map((ban) => (

- {databaseTimestampToDate(ban.createdAt).toLocaleString("en-US", { + {formatDateTime(databaseTimestampToDate(ban.createdAt), { year: "numeric", month: "long", day: "numeric", @@ -214,7 +218,7 @@ function BanLog() {

Banned till:{" "} {ban.banned !== 1 - ? databaseTimestampToDate(ban.banned).toLocaleString("en-US", { + ? formatDateTime(databaseTimestampToDate(ban.banned), { year: "numeric", month: "long", day: "numeric", @@ -240,6 +244,7 @@ function BanLog() { function FriendCodes() { const data = useLoaderData(); + const { formatDateTime } = useTimeFormat(); if (!data.friendCodes || data.friendCodes.length === 0) { return

No friend codes

; @@ -252,7 +257,7 @@ function FriendCodes() {

{fc.friendCode}

{index === 0 ? "Current" : "Past"} - Added on{" "} - {databaseTimestampToDate(fc.createdAt).toLocaleString("en-US", { + {formatDateTime(databaseTimestampToDate(fc.createdAt), { year: "numeric", month: "long", day: "numeric", diff --git a/app/features/user-page/routes/u.$identifier.seasons.tsx b/app/features/user-page/routes/u.$identifier.seasons.tsx index 659afb722..d68c00e25 100644 --- a/app/features/user-page/routes/u.$identifier.seasons.tsx +++ b/app/features/user-page/routes/u.$identifier.seasons.tsx @@ -42,6 +42,7 @@ import type { } from "~/features/sendouq-match/QMatchRepository.server"; import { useWeaponUsage } from "~/hooks/swr"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { modesShort } from "~/modules/in-game-lists/modes"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; @@ -180,7 +181,8 @@ function SeasonHeader({ seasonViewed: number; seasonsParticipatedIn: number[]; }) { - const { t, i18n } = useTranslation(["user"]); + const { t } = useTranslation(["user"]); + const { formatDate } = useTimeFormat(); const isMounted = useIsMounted(); const { starts, ends } = Seasons.nthToDateRange(seasonViewed); const navigate = useNavigate(); @@ -218,13 +220,13 @@ function SeasonHeader({ > {isMounted ? ( <> - {new Date(starts).toLocaleString(i18n.language, { + {formatDate(new Date(starts), { day: "numeric", month: "long", year: isDifferentYears ? "numeric" : undefined, })}{" "} -{" "} - {new Date(ends).toLocaleString(i18n.language, { + {formatDate(new Date(ends), { day: "numeric", month: "long", year: "numeric", @@ -667,6 +669,8 @@ function CanceledMatchesDialog({ }: { canceledMatches: NonNullable; }) { + const { formatDateTime } = useTimeFormat(); + return ( #{match.id}

- {databaseTimestampToDate(match.createdAt).toLocaleString()} + {formatDateTime(databaseTimestampToDate(match.createdAt))}
))} @@ -701,6 +705,7 @@ function Results({ results: UserSeasonsPageLoaderData["results"]; }) { const isMounted = useIsMounted(); + const { formatDate } = useTimeFormat(); const [, setSearchParams] = useSearchParams(); const ref = React.useRef(null); @@ -737,14 +742,11 @@ function Results({ )} > {isMounted - ? databaseTimestampToDate(result.createdAt).toLocaleString( - "en", - { - weekday: "long", - month: "long", - day: "numeric", - }, - ) + ? formatDate(databaseTimestampToDate(result.createdAt), { + weekday: "long", + month: "long", + day: "numeric", + }) : "t"}
{result.type === "GROUP_MATCH" ? ( diff --git a/app/features/vods/routes/vods.$id.tsx b/app/features/vods/routes/vods.$id.tsx index 49328e205..14cc5d4ea 100644 --- a/app/features/vods/routes/vods.$id.tsx +++ b/app/features/vods/routes/vods.$id.tsx @@ -13,6 +13,7 @@ import { YouTubeEmbed } from "~/components/YouTubeEmbed"; import { useUser } from "~/features/auth/core/user"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useSearchParamState } from "~/hooks/useSearchParamState"; +import { useTimeFormat } from "~/hooks/useTimeFormat"; import { databaseTimestampToDate } from "~/utils/dates"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; @@ -72,12 +73,12 @@ export default function VodPage() { defaultValue: 0, revive: Number, }); - const { i18n } = useTranslation(); const isMounted = useIsMounted(); const [autoplay, setAutoplay] = React.useState(false); const data = useLoaderData(); const { t } = useTranslation(["common", "vods"]); const user = useUser(); + const { formatDate } = useTimeFormat(); return (
@@ -98,9 +99,7 @@ export default function VodPage() { })} > {isMounted - ? databaseTimestampToDate( - data.vod.youtubeDate, - ).toLocaleDateString(i18n.language, { + ? formatDate(databaseTimestampToDate(data.vod.youtubeDate), { day: "numeric", month: "numeric", year: "numeric", diff --git a/app/hooks/useTimeFormat.ts b/app/hooks/useTimeFormat.ts new file mode 100644 index 000000000..51fcf8703 --- /dev/null +++ b/app/hooks/useTimeFormat.ts @@ -0,0 +1,88 @@ +import { useTranslation } from "react-i18next"; +import { useUser } from "~/features/auth/core/user"; + +const H12_TIME_OPTIONS: Intl.DateTimeFormatOptions = { + hour12: true, + hourCycle: "h12" as const, +}; +const H24_TIME_OPTIONS: Intl.DateTimeFormatOptions = { + hour12: false, + hourCycle: "h23" as const, + minute: "2-digit", +}; + +function getClockFormatOptions( + clockFormat: "auto" | "24h" | "12h" | undefined, + language: string, +): Intl.DateTimeFormatOptions { + if (!clockFormat || clockFormat === "auto") { + const isEnglish = language === "en"; + if (isEnglish) { + return H12_TIME_OPTIONS; + } + return H24_TIME_OPTIONS; + } + + if (clockFormat === "24h") { + return H24_TIME_OPTIONS; + } + + return H12_TIME_OPTIONS; +} + +/** + * Hook for formatting dates and times according to user preferences and locale. + * Respects the user's clock format preference (12h/24h) and current language. + * + * @example + * const { formatDateTime, formatTime, formatDate } = useTimeFormat(); + * + * // Format full date and time + * formatDateTime(new Date('2025-01-15T14:30:00')); + * // => "1/15/2025, 2:30 PM" (12h) or "1/15/2025, 14:30" (24h) + * + * // Format time only + * formatTime(new Date('2025-01-15T14:30:00')); + * // => "2:30 PM" (12h) or "14:30" (24h) + * + * // Format date only + * formatDate(new Date('2025-01-15')); + * // => "1/15/2025" + * + * // Custom options + * formatDateTime(new Date(), { dateStyle: 'full', timeStyle: 'short' }); + * // => "Wednesday, January 15, 2025 at 2:30 PM" + */ +export function useTimeFormat() { + const { i18n } = useTranslation(); + const user = useUser(); + const clockFormat = user?.preferences?.clockFormat; + const clockOptions = getClockFormatOptions(clockFormat, i18n.language); + + const formatDateTime = (date: Date, options?: Intl.DateTimeFormatOptions) => { + return date.toLocaleString( + i18n.language, + options?.hour + ? { + ...options, + ...clockOptions, + } + : { + ...options, + }, + ); + }; + + const formatTime = (date: Date, options?: Intl.DateTimeFormatOptions) => { + return date.toLocaleTimeString(i18n.language, { + ...options, + ...clockOptions, + }); + }; + + const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions) => { + return date.toLocaleDateString(i18n.language, options); + }; + + return { formatDateTime, formatTime, formatDate }; +} diff --git a/app/root.tsx b/app/root.tsx index 1daacf3d1..0b55de6fd 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -64,10 +64,11 @@ import "~/styles/vars.css"; export const shouldRevalidate: ShouldRevalidateFunction = (args) => { if (isRevalidation(args)) return true; - // // reload on language change so the selected language gets set into the cookie - const lang = args.nextUrl.searchParams.get("lng"); + // user settings, lang change etc. require revalidation on root loader + const isSettingsPage = args.currentUrl.pathname === "/settings"; + if (isSettingsPage) return true; - return Boolean(lang); + return false; }; export const meta: MetaFunction = (args) => { diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index 1ab168d43..0a9f96dad 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -38,4 +38,41 @@ test.describe("Settings", () => { expect(newContents).not.toBe(oldContents); }); + + test("updates clock format preference", async ({ page }) => { + await seed(page); + await impersonate(page); + + await navigate({ + page, + url: "/", + }); + + const tournamentCard = page.getByTestId("tournament-card").first(); + const timeElement = tournamentCard.locator("time"); + const initialTime = await timeElement.textContent(); + + expect(initialTime).toMatch(/AM|PM/); + + await navigate({ + page, + url: SETTINGS_PAGE, + }); + + const clockFormatSelect = page.locator("#clock-format"); + await clockFormatSelect.selectOption("24h"); + + await expect(page.getByText("Settings updated")).toBeVisible(); + + await navigate({ + page, + url: "/", + }); + + const newTime = await tournamentCard.locator("time").textContent(); + + expect(newTime).not.toMatch(/AM|PM/); + expect(newTime).not.toBe(initialTime); + expect(newTime).toContain(":"); + }); }); diff --git a/locales/da/common.json b/locales/da/common.json index 35ab54ad2..de714916c 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -306,6 +306,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/de/common.json b/locales/de/common.json index 6bf4fc348..87629f4e3 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -306,6 +306,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/en/common.json b/locales/en/common.json index 7851e99fd..0d527fe38 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -306,6 +306,10 @@ "settings.notifications.disableInfo": "To disable push notifications check your browser settings", "settings.notifications.browserNotSupported": "Push notifications are not supported on this browser", "settings.notifications.permissionDenied": "Push notifications were denied. Check your browser settings to re-enable", + "settings.clockFormat": "Clock format", + "clockFormat.auto": "Auto (use language default)", + "clockFormat.24h": "24-hour", + "clockFormat.12h": "12-hour (AM/PM)", "badges.selector.none": "No badges selected", "badges.selector.select": "Select badge to add", "api.title": "API Access", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 6097206a9..07553c336 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -308,6 +308,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "Ninguna insignia seleccionada", "badges.selector.select": "Seleccionar insignia añadir", "api.title": "", diff --git a/locales/es-US/common.json b/locales/es-US/common.json index 1630f0a3d..d3a7c7b75 100644 --- a/locales/es-US/common.json +++ b/locales/es-US/common.json @@ -308,6 +308,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "Ninguna insignia seleccionada", "badges.selector.select": "Seleccionar insignia añadir", "api.title": "", diff --git a/locales/fr-CA/common.json b/locales/fr-CA/common.json index 494e1e8ed..14eb0d2f8 100644 --- a/locales/fr-CA/common.json +++ b/locales/fr-CA/common.json @@ -308,6 +308,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/fr-EU/common.json b/locales/fr-EU/common.json index 1f4fb2d6f..c157f525a 100644 --- a/locales/fr-EU/common.json +++ b/locales/fr-EU/common.json @@ -308,6 +308,10 @@ "settings.notifications.disableInfo": "Regarder les paramètre de votre navigateur pour désactiver les notification push.", "settings.notifications.browserNotSupported": "Les notifications push ne sont pas supporter par cet navigateur", "settings.notifications.permissionDenied": "Les notifications push ont été refusées. Vérifiez les paramètres de votre navigateur pour les réactiver", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "Aucun badges est sélectionné", "badges.selector.select": "Selectionner un badge pour l'ajouter", "api.title": "", diff --git a/locales/he/common.json b/locales/he/common.json index a5f65637a..9baae4fc4 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -307,6 +307,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/it/common.json b/locales/it/common.json index 89569d130..0890dad78 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -308,6 +308,10 @@ "settings.notifications.disableInfo": "Per disattivare le notifiche push, controllare le impostazioni del browser", "settings.notifications.browserNotSupported": "Le notifiche push non sono supportate su questo browser", "settings.notifications.permissionDenied": "Le notifiche push sono state negate. Controlla le impostazioni del browser per riattivarle", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "Nessuna medaglia selezionata", "badges.selector.select": "Seleziona medaglia da aggiungere", "api.title": "", diff --git a/locales/ja/common.json b/locales/ja/common.json index b7660205d..598781c32 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -302,6 +302,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/ko/common.json b/locales/ko/common.json index 2c9e1a10c..ab02d7866 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -302,6 +302,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/nl/common.json b/locales/nl/common.json index d949e8230..5d496864e 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -306,6 +306,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/pl/common.json b/locales/pl/common.json index 131e91f8d..145efbbc4 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -309,6 +309,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 3c48ffcc4..1aed17756 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -308,6 +308,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "", "badges.selector.select": "", "api.title": "", diff --git a/locales/ru/common.json b/locales/ru/common.json index d103a018d..8dd9193cd 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -309,6 +309,10 @@ "settings.notifications.disableInfo": "Чтобы отключить push-уведомления проверьте настройки вашего браузера", "settings.notifications.browserNotSupported": "Push-уведомления в вашем браузере не поддерживаются", "settings.notifications.permissionDenied": "Push-уведомления были запрещены. Проверьте настройки вашего бразуера, чтобы включить их обратно", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "Награды не выбраны", "badges.selector.select": "Выберите награды для добавления", "api.title": "", diff --git a/locales/zh/common.json b/locales/zh/common.json index b29e617eb..39893ad6d 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -302,6 +302,10 @@ "settings.notifications.disableInfo": "", "settings.notifications.browserNotSupported": "", "settings.notifications.permissionDenied": "", + "settings.clockFormat": "", + "clockFormat.auto": "", + "clockFormat.24h": "", + "clockFormat.12h": "", "badges.selector.none": "没有选择任何徽章", "badges.selector.select": "选择徽章并添加", "api.title": "",