sendou.ink/app/utils/dates.ts
Kalle db6ccf254b
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run
Convert new calendar event/tournament page to SendouForm
2026-06-19 11:49:11 +03:00

239 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
CalendarDate,
CalendarDateTime,
parseDate,
} from "@internationalized/date";
import type { Locale } from "date-fns";
import { formatDistanceToNow as dateFnsFormatDistanceToNow } from "date-fns";
import { da } from "date-fns/locale/da";
import { de } from "date-fns/locale/de";
import { enUS } from "date-fns/locale/en-US";
import { es } from "date-fns/locale/es";
import { fr } from "date-fns/locale/fr";
import { frCA } from "date-fns/locale/fr-CA";
import { he } from "date-fns/locale/he";
import { it } from "date-fns/locale/it";
import { ja } from "date-fns/locale/ja";
import { ko } from "date-fns/locale/ko";
import { nl } from "date-fns/locale/nl";
import { pl } from "date-fns/locale/pl";
import { ptBR } from "date-fns/locale/pt-BR";
import { ru } from "date-fns/locale/ru";
import { zhCN } from "date-fns/locale/zh-CN";
import type { MonthYear } from "~/features/plus-voting/core";
import type { LanguageCode } from "~/modules/i18n/config";
import type { DayMonthYear } from "./zod";
const LOCALE_MAP: Record<LanguageCode, Locale> = {
da,
de,
en: enUS,
"es-ES": es,
"es-US": es,
"fr-CA": frCA,
"fr-EU": fr,
he,
it,
ja,
ko,
nl,
pl,
"pt-BR": ptBR,
ru,
zh: zhCN,
};
export function formatDistanceToNow(
date: Parameters<typeof dateFnsFormatDistanceToNow>[0],
options: Omit<
NonNullable<Parameters<typeof dateFnsFormatDistanceToNow>[1]>,
"locale"
> & { language: LanguageCode },
) {
return dateFnsFormatDistanceToNow(date, {
...options,
locale: LOCALE_MAP[options.language],
});
}
export function databaseTimestampToDate(timestamp: number) {
return new Date(databaseTimestampToJavascriptTimestamp(timestamp));
}
export function databaseTimestampToJavascriptTimestamp(timestamp: number) {
return timestamp * 1000;
}
export function dateToDatabaseTimestamp(date: Date) {
return Math.floor(date.getTime() / 1000);
}
export function databaseTimestampNow() {
return dateToDatabaseTimestamp(new Date());
}
/**
* Converts a date represented by day, month, and year into a JavaScript Date object, noon UTC.
*/
export function dayMonthYearToDate({ day, month, year }: DayMonthYear) {
return new Date(Date.UTC(year, month, day, 12));
}
/**
* Converts a JavaScript Date object into a CalendarDateTime object (used by react-aria-components).
*/
export function dateToDateValue(date: Date) {
return new CalendarDateTime(
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
);
}
/**
* Converts a JavaScript Date object into a CalendarDate object (used by react-aria-components for date-only pickers).
*/
export function dateToCalendarDate(date: Date) {
return new CalendarDate(
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
);
}
/**
* Converts a date represented by day, month, and year into a DateValue object (used by react-aria-components), noon UTC.
*/
export function dayMonthYearToDateValue({ day, month, year }: DayMonthYear) {
const isoString = dateToYYYYMMDD(new Date(Date.UTC(year, month, day, 12)));
return parseDate(isoString);
}
/**
* Converts a date represented by day, month, and year into a database timestamp, noon UTC.
*/
export function dayMonthYearToDatabaseTimestamp(args: DayMonthYear) {
return dateToDatabaseTimestamp(dayMonthYearToDate(args));
}
// https://stackoverflow.com/a/71336659
export function weekNumberToDate({
week,
year,
position = "start",
}: {
week: number;
year: number;
/** start = Date of Monday, end = Date of Sunday */
position?: "start" | "end";
}) {
const result = new Date(Date.UTC(year, 0, 4));
result.setUTCDate(
result.getUTCDate() - (result.getUTCDay() || 7) + 1 + 7 * (week - 1),
);
if (position === "end") {
result.setUTCDate(result.getUTCDate() + 6);
}
return result;
}
/**
* Returns the UTC date range covering an ISO week: the Monday that starts the
* week and the Monday that starts the following week (a 7-day span). Uses UTC
* date arithmetic so the span is exactly 7×24h regardless of the server's
* timezone or any DST transition that falls inside the week.
*/
export function weekNumberToDateRange({
week,
year,
}: {
week: number;
year: number;
}) {
const startTime = weekNumberToDate({ week, year });
const endTime = new Date(startTime);
endTime.setUTCDate(endTime.getUTCDate() + 7);
return { startTime, endTime };
}
/**
* Checks if a date is valid or not.
*
* Returns:
* - True if date is valid
* - False otherwise
*/
export function isValidDate(date: Date) {
return !Number.isNaN(date.getTime());
}
/** Returns date as a string with the format YYYY-MM-DDThh:mm in user's time zone */
export function dateToYearMonthDayHourMinuteString(date: Date) {
const copiedDate = new Date(date.getTime());
if (!isValidDate(copiedDate)) {
throw new Error("tried to format string from invalid date");
}
const year = copiedDate.getFullYear();
const month = copiedDate.getMonth() + 1;
const day = copiedDate.getDate();
const hour = copiedDate.getHours();
const minute = copiedDate.getMinutes();
return `${year}-${prefixZero(month)}-${prefixZero(day)}T${prefixZero(
hour,
)}:${prefixZero(minute)}`;
}
function prefixZero(number: number) {
return number < 10 ? `0${number}` : number;
}
export function getDateAtNextFullHour(date: Date) {
const copiedDate = new Date(date.getTime());
if (
date.getMinutes() > 0 ||
date.getSeconds() > 0 ||
date.getMilliseconds() > 0
) {
copiedDate.setHours(date.getHours() + 1);
copiedDate.setMinutes(0);
}
copiedDate.setSeconds(0);
copiedDate.setMilliseconds(0);
return copiedDate;
}
export function dateToYYYYMMDD(date: Date) {
return date.toISOString().split("T")[0];
}
// same as datesOfMonth but contains null at the start to start with monday
export function nullPaddedDatesOfMonth({ month, year }: MonthYear) {
const dates = datesOfMonth({ month, year });
const firstDay = dates[0].getUTCDay();
const nulls = Array.from(
{ length: firstDay === 0 ? 6 : firstDay - 1 },
() => null,
);
return [...nulls, ...dates];
}
function datesOfMonth({ month, year }: MonthYear) {
const dates = [];
const date = new Date(Date.UTC(year, month, 1));
while (date.getUTCMonth() === month) {
dates.push(new Date(date));
date.setUTCDate(date.getUTCDate() + 1);
}
return dates;
}