mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-24 03:55:49 -05:00
Add "Date format" preference
This commit is contained in:
@@ -986,6 +986,15 @@ export interface UserPreferences {
|
||||
* "12h" = 12 hour format (e.g. 2:00 PM)
|
||||
* */
|
||||
clockFormat?: "24h" | "12h" | "auto";
|
||||
/**
|
||||
* What numeric date format the user prefers?
|
||||
*
|
||||
* "auto" = use the format the active language defaults to (default value)
|
||||
* "MDY" = month/day/year (e.g. 4/27/2026)
|
||||
* "DMY" = day/month/year (e.g. 27/04/2026)
|
||||
* "YMD" = ISO year-month-day (e.g. 2026-04-27)
|
||||
* */
|
||||
dateFormat?: "auto" | "MDY" | "DMY" | "YMD";
|
||||
/** Is the new widget based user page enabled? (Supporter early preview) */
|
||||
newProfileEnabled?: boolean;
|
||||
/** Is spoiler-free mode enabled? Hides recent tournament results and scores until the user chooses to reveal them. */
|
||||
|
||||
@@ -59,6 +59,12 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UPDATE_DATE_FORMAT": {
|
||||
await UserRepository.updatePreferences(user.id, {
|
||||
dateFormat: data.newValue,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { action } from "../actions/settings.server";
|
||||
import { loader } from "../loaders/settings.server";
|
||||
import {
|
||||
clockFormatSchema,
|
||||
dateFormatSchema,
|
||||
disableBuildAbilitySortingSchema,
|
||||
disallowScrimPickupsFromUntrustedSchema,
|
||||
spoilerFreeModeSchema,
|
||||
@@ -88,6 +89,18 @@ export default function SettingsPage() {
|
||||
{({ FormField }) => <FormField name="newValue" />}
|
||||
</SendouForm>
|
||||
) : null}
|
||||
{user ? (
|
||||
<SendouForm
|
||||
schema={dateFormatSchema}
|
||||
defaultValues={{
|
||||
newValue: user.preferences.dateFormat ?? "auto",
|
||||
}}
|
||||
autoSubmit
|
||||
revalidateRoot
|
||||
>
|
||||
{({ FormField }) => <FormField name="newValue" />}
|
||||
</SendouForm>
|
||||
) : null}
|
||||
{user ? (
|
||||
<>
|
||||
<Divider className={styles.divider} smallText>
|
||||
|
||||
@@ -19,6 +19,19 @@ export const clockFormatSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const dateFormatSchema = z.object({
|
||||
_action: stringConstant("UPDATE_DATE_FORMAT"),
|
||||
newValue: select({
|
||||
label: "labels.dateFormat",
|
||||
items: [
|
||||
{ value: "auto", label: "options.dateFormat.auto" },
|
||||
{ value: "MDY", label: "options.dateFormat.MDY" },
|
||||
{ value: "DMY", label: "options.dateFormat.DMY" },
|
||||
{ value: "YMD", label: "options.dateFormat.YMD" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
export const disableBuildAbilitySortingSchema = z.object({
|
||||
_action: stringConstant("UPDATE_DISABLE_BUILD_ABILITY_SORTING"),
|
||||
newValue: toggle({
|
||||
@@ -58,4 +71,5 @@ export const settingsEditSchema = z.union([
|
||||
spoilerFreeModeSchema,
|
||||
updateNoScreenSchema,
|
||||
clockFormatSchema,
|
||||
dateFormatSchema,
|
||||
]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { UserPreferences } from "~/db/tables";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { LanguageCode } from "~/modules/i18n/config";
|
||||
import { formatDistanceToNow as formatDistanceToNowUtil } from "~/utils/dates";
|
||||
@@ -11,6 +12,15 @@ const H24_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||
hour12: false,
|
||||
hourCycle: "h23" as const,
|
||||
};
|
||||
|
||||
const DATE_FORMAT_LOCALE: Record<
|
||||
Exclude<NonNullable<UserPreferences["dateFormat"]>, "auto">,
|
||||
string
|
||||
> = {
|
||||
MDY: "en-US",
|
||||
DMY: "en-GB",
|
||||
YMD: "sv-SE",
|
||||
};
|
||||
function getClockFormatOptions(
|
||||
clockFormat: "auto" | "24h" | "12h" | undefined,
|
||||
language: string,
|
||||
@@ -68,11 +78,13 @@ export function useTimeFormat() {
|
||||
const { i18n } = useTranslation();
|
||||
const user = useUser();
|
||||
const clockFormat = user?.preferences?.clockFormat;
|
||||
const dateFormat = user?.preferences?.dateFormat;
|
||||
const clockOptions = getClockFormatOptions(clockFormat, i18n.language);
|
||||
const dateLocale = getDateLocale(dateFormat, i18n.language);
|
||||
|
||||
const formatDateTime = (date: Date, options?: Intl.DateTimeFormatOptions) => {
|
||||
const result = date.toLocaleString(
|
||||
i18n.language,
|
||||
isNumericMonth(options) ? dateLocale : i18n.language,
|
||||
options?.hour
|
||||
? {
|
||||
...options,
|
||||
@@ -104,7 +116,10 @@ export function useTimeFormat() {
|
||||
};
|
||||
|
||||
const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions) => {
|
||||
return date.toLocaleDateString(i18n.language, options);
|
||||
return date.toLocaleDateString(
|
||||
isNumericMonth(options) ? dateLocale : i18n.language,
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
/** Same as `formatDateTime` but omits minutes when they are zero and AM/PM format is in use */
|
||||
@@ -165,3 +180,16 @@ export function useTimeFormat() {
|
||||
function stripLeadingZeroFromHour(timeString: string) {
|
||||
return timeString.replace(/\b0(\d:\d{2})/g, "$1");
|
||||
}
|
||||
|
||||
function getDateLocale(
|
||||
dateFormat: UserPreferences["dateFormat"] | undefined,
|
||||
language: string,
|
||||
) {
|
||||
if (!dateFormat || dateFormat === "auto") return language;
|
||||
return DATE_FORMAT_LOCALE[dateFormat];
|
||||
}
|
||||
|
||||
function isNumericMonth(options: Intl.DateTimeFormatOptions | undefined) {
|
||||
if (!options?.month) return false;
|
||||
return options.month === "numeric" || options.month === "2-digit";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "Tag",
|
||||
"labels.teamBsky": "Team Bluesky",
|
||||
"labels.clockFormat": "Clock format",
|
||||
"labels.dateFormat": "Date format",
|
||||
"labels.disableBuildAbilitySorting": "Builds: Disable automatic ability sorting",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "Disallow scrim pickups from non-friends",
|
||||
"labels.noScreen": "[Accessibility] Avoid Splattercolor Screen",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "Automatic",
|
||||
"options.clockFormat.24h": "24-hour",
|
||||
"options.clockFormat.12h": "12-hour",
|
||||
"options.dateFormat.auto": "Automatic",
|
||||
"options.dateFormat.MDY": "MM/DD/YYYY",
|
||||
"options.dateFormat.DMY": "DD/MM/YYYY",
|
||||
"options.dateFormat.YMD": "YYYY-MM-DD",
|
||||
"errors.required": "This field is required",
|
||||
"errors.minLength": "Must be at least {{min}} characters",
|
||||
"errors.maxLength": "Must be at most {{max}} characters",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "Etiqueta",
|
||||
"labels.teamBsky": "Bluesky del equipo",
|
||||
"labels.clockFormat": "Formato de hora",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "Builds: Desactivar orden automático de potenciadores",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "No permitir invitaciones de usuarios no verificados",
|
||||
"labels.noScreen": "[Accesibilidad] Evitar Pantintalla",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "Automático",
|
||||
"options.clockFormat.24h": "24 horas",
|
||||
"options.clockFormat.12h": "12 horas",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "Este campo es obligatorio",
|
||||
"errors.minLength": "Debe tener al menos {{min}} caracteres",
|
||||
"errors.maxLength": "Debe tener como máximo {{max}} caracteres",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "Team Bluesky",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "Bluesky del team",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "チームの Bluesky",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "Bluesky команды",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.dateFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
@@ -18,6 +19,10 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.dateFormat.auto": "",
|
||||
"options.dateFormat.MDY": "",
|
||||
"options.dateFormat.DMY": "",
|
||||
"options.dateFormat.YMD": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
|
||||
Reference in New Issue
Block a user