Refactor DateTimeFormField to use RAC based input Closes #2359

This commit is contained in:
Kalle
2025-06-03 17:33:57 +03:00
parent db374bfc29
commit 242f20aeb1
7 changed files with 51 additions and 49 deletions

View File

@@ -1,10 +1,11 @@
import type { CalendarDateTime } from "@internationalized/date";
import {
Controller,
type FieldPath,
type FieldValues,
useFormContext,
} from "react-hook-form";
import { dayMonthYearToDateValue } from "../../utils/dates";
import { dateToDateValue, dayMonthYearToDateValue } from "../../utils/dates";
import type { DayMonthYear } from "../../utils/zod";
import { SendouDatePicker } from "../elements/DatePicker";
import type { FormFieldSize } from "./form-utils";
@@ -15,12 +16,14 @@ export function DateFormField<T extends FieldValues>({
bottomText,
required,
size,
granularity = "day",
}: {
label: string;
name: FieldPath<T>;
bottomText?: string;
required?: boolean;
size?: FormFieldSize;
granularity?: "day" | "minute";
}) {
const methods = useFormContext();
@@ -33,17 +36,21 @@ export function DateFormField<T extends FieldValues>({
fieldState: { invalid, error },
}) => {
const getValue = () => {
const originalValue = value as DayMonthYear | null;
const originalValue = value as DayMonthYear | Date | null;
if (!originalValue) return null;
return dayMonthYearToDateValue(originalValue);
if (originalValue instanceof Date) {
return dateToDateValue(originalValue);
}
return dayMonthYearToDateValue(originalValue as DayMonthYear);
};
return (
<SendouDatePicker
label={label}
granularity="day"
granularity={granularity}
isRequired={required}
errorText={error?.message as string | undefined}
value={getValue()}
@@ -53,11 +60,23 @@ export function DateFormField<T extends FieldValues>({
onBlur={onBlur}
onChange={(value) => {
if (value) {
onChange({
day: value.day,
month: value.month - 1,
year: value.year,
});
if (granularity === "minute") {
onChange(
new Date(
value.year,
value.month - 1,
value.day,
(value as CalendarDateTime).hour,
(value as CalendarDateTime).minute,
),
);
} else {
onChange({
day: value.day,
month: value.month - 1,
year: value.year,
});
}
}
if (!value) {

View File

@@ -1,33 +0,0 @@
import * as React from "react";
import {
type FieldPath,
type FieldValues,
get,
useFormContext,
} from "react-hook-form";
import { FormMessage } from "~/components/FormMessage";
import { Label } from "~/components/Label";
export function DateTimeFormField<T extends FieldValues>({
label,
name,
bottomText,
}: { label: string; name: FieldPath<T>; bottomText?: string }) {
const methods = useFormContext();
const id = React.useId();
const error = get(methods.formState.errors, name);
return (
<div>
<Label htmlFor={id}>{label}</Label>
<input id={id} {...methods.register(name)} type="datetime-local" />
{error && (
<FormMessage type="error">{error.message as string}</FormMessage>
)}
{bottomText && !error ? (
<FormMessage type="info">{bottomText}</FormMessage>
) : null}
</div>
);
}

View File

@@ -4,7 +4,7 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { z } from "zod";
import { Label } from "~/components/Label";
import { DateTimeFormField } from "~/components/form/DateTimeFormField";
import { DateFormField } from "~/components/form/DateFormField";
import { SendouForm } from "~/components/form/SendouForm";
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
import { nullFilledArray } from "~/utils/arrays";
@@ -62,10 +62,11 @@ export default function NewScrimPage() {
>
<WithFormField usersTeams={data.teams} />
<DateTimeFormField<FormFields>
<DateFormField<FormFields>
label={t("scrims:forms.when.title")}
name="at"
bottomText={t("scrims:forms.when.explanation")}
granularity="minute"
/>
<BaseVisibilityFormField associations={data.associations} />
@@ -143,9 +144,10 @@ function NotFoundVisibilityFormField({
return (
<div>
<div className="stack horizontal sm">
<DateTimeFormField<FormFields>
<DateFormField<FormFields>
label={t("scrims:forms.notFoundVisibility.title")}
name="notFoundVisibility.at"
granularity="minute"
/>
{date ? (
<div>

View File

@@ -1,4 +1,4 @@
import { parseDate } from "@internationalized/date";
import { CalendarDateTime, parseDate } from "@internationalized/date";
import { getWeek } from "date-fns";
import type { MonthYear } from "~/features/plus-voting/core";
import type { DayMonthYear } from "./zod";
@@ -26,6 +26,20 @@ 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 date represented by day, month, and year into a DateValue object (used by react-aria-components), noon UTC.
*/

View File

@@ -26,7 +26,7 @@
"forms.with.user": "Nutzer {{nth}}",
"forms.with.pick-up": "Pick-up",
"forms.when.title": "Wann",
"forms.when.explanation": "Leer lassen, wenn du jetzt nach einem Scrim suchen willst.",
"forms.when.explanation": "",
"forms.text.title": "Text",
"forms.visibility.title": "Sichtbarkeit",
"forms.visibility.public": "Öffentlich",

View File

@@ -26,7 +26,7 @@
"forms.with.user": "User {{nth}}",
"forms.with.pick-up": "Pick-up",
"forms.when.title": "When",
"forms.when.explanation": "Leave blank if you want to look for a scrim now",
"forms.when.explanation": "Leave to default if you want to look for a scrim now",
"forms.text.title": "Text",
"forms.visibility.title": "Visibility",
"forms.visibility.public": "Public",

View File

@@ -26,7 +26,7 @@
"forms.with.user": "Utilisateur {{nth}}",
"forms.with.pick-up": "Pick-up",
"forms.when.title": "Quand",
"forms.when.explanation": "Laissez vide si vous voulez chercher un scrim dès maintenant",
"forms.when.explanation": "",
"forms.text.title": "Texte",
"forms.visibility.title": "Visibilité",
"forms.visibility.public": "Public",