mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Strongly type form components
Strongly type form components
This commit is contained in:
@@ -74,17 +74,24 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-4">
|
<div className="col-span-4">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.tournament"
|
name="tournament.tournament"
|
||||||
|
control={form.control}
|
||||||
label={t("tournamentNameLabel")}
|
label={t("tournamentNameLabel")}
|
||||||
placeholder={t("tournamentNamePlaceholder")}
|
placeholder={t("tournamentNamePlaceholder")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<DateField name="tournament.date" label={t("dateLabel")} required />
|
<DateField
|
||||||
|
name="tournament.date"
|
||||||
|
control={form.control}
|
||||||
|
label={t("dateLabel")}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.url"
|
name="tournament.url"
|
||||||
|
control={form.control}
|
||||||
label={t("urlLabel")}
|
label={t("urlLabel")}
|
||||||
placeholder={t("urlPlaceholder")}
|
placeholder={t("urlPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -93,6 +100,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<SelectField
|
<SelectField
|
||||||
name="tournament.time_control"
|
name="tournament.time_control"
|
||||||
|
control={form.control}
|
||||||
label={t("tcLabel")}
|
label={t("tcLabel")}
|
||||||
options={[
|
options={[
|
||||||
TimeControl.Classic,
|
TimeControl.Classic,
|
||||||
@@ -108,12 +116,14 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<SwitchField
|
<SwitchField
|
||||||
name="tournament.norm_tournament"
|
name="tournament.norm_tournament"
|
||||||
|
control={form.control}
|
||||||
label={t("normLabel")}
|
label={t("normLabel")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4">
|
<div className="col-span-4">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.address"
|
name="tournament.address"
|
||||||
|
control={form.control}
|
||||||
label={t("addressLabel")}
|
label={t("addressLabel")}
|
||||||
placeholder={t("addressPlaceholder")}
|
placeholder={t("addressPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -122,6 +132,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-4 sm:col-span-2">
|
<div className="col-span-4 sm:col-span-2">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.town"
|
name="tournament.town"
|
||||||
|
control={form.control}
|
||||||
label={t("townLabel")}
|
label={t("townLabel")}
|
||||||
placeholder={t("townPlaceholder")}
|
placeholder={t("townPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -130,6 +141,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.department"
|
name="tournament.department"
|
||||||
|
control={form.control}
|
||||||
label={t("departmentLabel")}
|
label={t("departmentLabel")}
|
||||||
placeholder={t("departmentPlaceholder")}
|
placeholder={t("departmentPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -138,6 +150,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.country"
|
name="tournament.country"
|
||||||
|
control={form.control}
|
||||||
label={t("countryLabel")}
|
label={t("countryLabel")}
|
||||||
placeholder={t("countryPlaceholder")}
|
placeholder={t("countryPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -146,6 +159,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="name"
|
name="name"
|
||||||
|
control={form.control}
|
||||||
label={t("yourNameLabel")}
|
label={t("yourNameLabel")}
|
||||||
placeholder={t("yourNamePlaceholder")}
|
placeholder={t("yourNamePlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -154,6 +168,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="email"
|
name="email"
|
||||||
|
control={form.control}
|
||||||
label={t("yourEmailLabel")}
|
label={t("yourEmailLabel")}
|
||||||
placeholder={t("yourEmailPlaceholder")}
|
placeholder={t("yourEmailPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -162,6 +177,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-4 row-span-2 sm:col-span-2">
|
<div className="col-span-4 row-span-2 sm:col-span-2">
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
name="message"
|
name="message"
|
||||||
|
control={form.control}
|
||||||
label={t("messageLabel")}
|
label={t("messageLabel")}
|
||||||
rows={6}
|
rows={6}
|
||||||
placeholder={t("messagePlaceholder")}
|
placeholder={t("messagePlaceholder")}
|
||||||
@@ -171,6 +187,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.coordinates.0"
|
name="tournament.coordinates.0"
|
||||||
|
control={form.control}
|
||||||
label={t("latLabel")}
|
label={t("latLabel")}
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
@@ -179,6 +196,7 @@ const TournamentForm = () => {
|
|||||||
<div className="col-span-2 sm:col-span-1">
|
<div className="col-span-2 sm:col-span-1">
|
||||||
<TextField
|
<TextField
|
||||||
name="tournament.coordinates.1"
|
name="tournament.coordinates.1"
|
||||||
|
control={form.control}
|
||||||
label={t("lngLabel")}
|
label={t("lngLabel")}
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const ContactForm = () => {
|
|||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
name="email"
|
name="email"
|
||||||
|
control={form.control}
|
||||||
label={t("emailLabel")}
|
label={t("emailLabel")}
|
||||||
placeholder={t("emailPlaceholder")}
|
placeholder={t("emailPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -62,6 +63,7 @@ const ContactForm = () => {
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
name="subject"
|
name="subject"
|
||||||
|
control={form.control}
|
||||||
label={t("subjectLabel")}
|
label={t("subjectLabel")}
|
||||||
placeholder={t("subjectPlaceholder")}
|
placeholder={t("subjectPlaceholder")}
|
||||||
required
|
required
|
||||||
@@ -69,6 +71,7 @@ const ContactForm = () => {
|
|||||||
|
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
name="message"
|
name="message"
|
||||||
|
control={form.control}
|
||||||
rows={6}
|
rows={6}
|
||||||
label={t("messageLabel")}
|
label={t("messageLabel")}
|
||||||
placeholder={t("messagePlaceholder")}
|
placeholder={t("messagePlaceholder")}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export const ManualEloForm = () => {
|
|||||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<TextField
|
<TextField
|
||||||
name="currentElo"
|
name="currentElo"
|
||||||
|
control={form.control}
|
||||||
label={t("currentEloLabel")}
|
label={t("currentEloLabel")}
|
||||||
placeholder={t("currentEloPlaceholder")}
|
placeholder={t("currentEloPlaceholder")}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -115,6 +116,7 @@ export const ManualEloForm = () => {
|
|||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
name="kFactor"
|
name="kFactor"
|
||||||
|
control={form.control}
|
||||||
label={t("yourKFactorLabel")}
|
label={t("yourKFactorLabel")}
|
||||||
options={["40", "20", "10"].map((k) => ({
|
options={["40", "20", "10"].map((k) => ({
|
||||||
value: k,
|
value: k,
|
||||||
@@ -139,6 +141,7 @@ export const ManualEloForm = () => {
|
|||||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<TextField
|
<TextField
|
||||||
name={`games.${i}.opponentElo`}
|
name={`games.${i}.opponentElo`}
|
||||||
|
control={form.control}
|
||||||
placeholder={t("opponentEloPlaceholder")}
|
placeholder={t("opponentEloPlaceholder")}
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
@@ -147,6 +150,7 @@ export const ManualEloForm = () => {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupField
|
<RadioGroupField
|
||||||
name={`games.${i}.result`}
|
name={`games.${i}.result`}
|
||||||
|
control={form.control}
|
||||||
options={["win", "draw", "loss"].map((result) => ({
|
options={["win", "draw", "loss"].map((result) => ({
|
||||||
value: result,
|
value: result,
|
||||||
label: t("gameResult", { result }),
|
label: t("gameResult", { result }),
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export default function Elo() {
|
|||||||
<div className="flex items-end gap-4">
|
<div className="flex items-end gap-4">
|
||||||
<TextField
|
<TextField
|
||||||
name="url"
|
name="url"
|
||||||
|
control={form.control}
|
||||||
label={t("resultsUrlLabel")}
|
label={t("resultsUrlLabel")}
|
||||||
placeholder={t("resultsUrlLabel")}
|
placeholder={t("resultsUrlLabel")}
|
||||||
/>
|
/>
|
||||||
@@ -222,6 +223,7 @@ export default function Elo() {
|
|||||||
<div className="grid w-full grid-cols-1 gap-2 sm:grid-cols-2">
|
<div className="grid w-full grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
name="player"
|
name="player"
|
||||||
|
control={form.control}
|
||||||
required
|
required
|
||||||
label={t("choosePlayerLabel")}
|
label={t("choosePlayerLabel")}
|
||||||
placeholder={t("choosePlayerPlaceholder")}
|
placeholder={t("choosePlayerPlaceholder")}
|
||||||
@@ -231,6 +233,7 @@ export default function Elo() {
|
|||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
name="kFactor"
|
name="kFactor"
|
||||||
|
control={form.control}
|
||||||
label={t("kFactorLabel")}
|
label={t("kFactorLabel")}
|
||||||
options={["40", "20", "10"].map((k) => ({
|
options={["40", "20", "10"].map((k) => ({
|
||||||
value: k,
|
value: k,
|
||||||
|
|||||||
@@ -6,26 +6,41 @@ import { get, range } from "lodash";
|
|||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import { registerLocale } from "react-datepicker";
|
import { registerLocale } from "react-datepicker";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Field, FieldProps } from "../Field";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
|
import { Field, GenericFieldProps } from "../Field";
|
||||||
|
|
||||||
import { InputDatePicker } from "./components";
|
import { InputDatePicker } from "./components";
|
||||||
import { DatePickerCustomHeader } from "./components/DatePickerCustomHeader";
|
import { DatePickerCustomHeader } from "./components/DatePickerCustomHeader";
|
||||||
|
|
||||||
registerLocale("fr", fr);
|
registerLocale("fr", fr);
|
||||||
|
|
||||||
type DateFieldProps = FieldProps & {
|
type DateFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> & {
|
||||||
maxDate?: Date;
|
maxDate?: Date;
|
||||||
minDate?: Date;
|
minDate?: Date;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
datePickerPopperClass?: string;
|
datePickerPopperClass?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
};
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export const DateField = ({
|
export const DateField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
minDate,
|
minDate,
|
||||||
maxDate,
|
maxDate,
|
||||||
dateFormat = "dd/MM/yyyy",
|
dateFormat = "dd/MM/yyyy",
|
||||||
@@ -33,15 +48,17 @@ export const DateField = ({
|
|||||||
datePickerPopperClass,
|
datePickerPopperClass,
|
||||||
|
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
|
required,
|
||||||
...otherFieldProps
|
...otherFieldProps
|
||||||
}: DateFieldProps) => {
|
}: DateFieldProps<TFieldValues, TFieldName>) => {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const at = useTranslations("App");
|
const at = useTranslations("App");
|
||||||
const min = minDate ? minDate.getFullYear() : 1900;
|
const min = minDate ? minDate.getFullYear() : 1900;
|
||||||
|
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = form;
|
} = form;
|
||||||
@@ -49,10 +66,10 @@ export const DateField = ({
|
|||||||
const hasError = name && !!get(errors, name)?.message;
|
const hasError = name && !!get(errors, name)?.message;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field name={name} {...otherFieldProps}>
|
<Field name={name} control={control} {...otherFieldProps}>
|
||||||
<div className={twMerge("relative flex w-full flex-col", className)}>
|
<div className={twMerge("relative flex w-full flex-col", className)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
import { useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Control,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
import { ErrorMessage } from "./ErrorMessage";
|
import { ErrorMessage } from "./ErrorMessage";
|
||||||
import { Label } from "./Label";
|
import { Label } from "./Label";
|
||||||
|
|
||||||
export type FieldProps = {
|
export type GenericFieldProps<
|
||||||
name: string;
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TFieldName;
|
||||||
|
control: Control<TFieldValues>;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
childrenWrapperClassName?: string;
|
childrenWrapperClassName?: string;
|
||||||
@@ -16,12 +27,21 @@ export type FieldProps = {
|
|||||||
hideErrorMessage?: boolean;
|
hideErrorMessage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Field = (
|
type FieldProps<
|
||||||
props: Omit<FieldProps, "name"> & {
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> & {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
name?: string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
},
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const Field = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: FieldProps<TFieldValues, TFieldName>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
@@ -32,9 +52,12 @@ export const Field = (
|
|||||||
children,
|
children,
|
||||||
required,
|
required,
|
||||||
hideErrorMessage = false,
|
hideErrorMessage = false,
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used to restrict the `name` prop type
|
||||||
|
control,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
type LabelProps = React.DetailedHTMLProps<
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
|
type LabelProps = Prettify<
|
||||||
|
React.DetailedHTMLProps<
|
||||||
React.LabelHTMLAttributes<HTMLLabelElement>,
|
React.LabelHTMLAttributes<HTMLLabelElement>,
|
||||||
HTMLLabelElement
|
HTMLLabelElement
|
||||||
> & {
|
> & {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
tooltip?: React.ReactNode;
|
tooltip?: React.ReactNode;
|
||||||
};
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export const Label = ({
|
export const Label = ({
|
||||||
required,
|
required,
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { RadioGroup } from "@headlessui/react";
|
import { RadioGroup } from "@headlessui/react";
|
||||||
import { get, isNil } from "lodash";
|
import { isNil } from "lodash";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Field, FieldProps } from "./Field";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
|
|
||||||
export type BaseOption<T = string, D = unknown> = {
|
export type BaseOption<T = string, D = unknown> = {
|
||||||
value: T;
|
value: T;
|
||||||
@@ -15,13 +22,26 @@ export type BaseOption<T = string, D = unknown> = {
|
|||||||
data?: D;
|
data?: D;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RadioGroupFieldProps<T = string, D = unknown> = FieldProps & {
|
export type RadioGroupFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
T = string,
|
||||||
|
D = unknown,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> & {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
options: BaseOption<T, D>[];
|
options: BaseOption<T, D>[];
|
||||||
};
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export const RadioGroupField = <T extends React.Key = string, D = unknown>({
|
export const RadioGroupField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
T extends React.Key = string,
|
||||||
|
D = unknown,
|
||||||
|
>({
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
className,
|
className,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
childrenWrapperClassName,
|
childrenWrapperClassName,
|
||||||
@@ -30,16 +50,14 @@ export const RadioGroupField = <T extends React.Key = string, D = unknown>({
|
|||||||
required,
|
required,
|
||||||
|
|
||||||
options,
|
options,
|
||||||
}: RadioGroupFieldProps<T, D>) => {
|
}: RadioGroupFieldProps<TFieldValues, TFieldName, T, D>) => {
|
||||||
const t = useTranslations("App");
|
const t = useTranslations("App");
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const hasError = !!get(errors, name)?.message;
|
|
||||||
|
|
||||||
const valueToOption = (value: T): BaseOption<T, D> =>
|
const valueToOption = (value: T): BaseOption<T, D> =>
|
||||||
options.find((o) => o.value === value) ?? { value, label: "" };
|
options.find((o) => o.value === value) ?? { value, label: "" };
|
||||||
|
|
||||||
@@ -47,6 +65,7 @@ export const RadioGroupField = <T extends React.Key = string, D = unknown>({
|
|||||||
<Field
|
<Field
|
||||||
{...{
|
{...{
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
className,
|
className,
|
||||||
label,
|
label,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
@@ -57,7 +76,7 @@ export const RadioGroupField = <T extends React.Key = string, D = unknown>({
|
|||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name={name}
|
name={name}
|
||||||
control={form.control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const optionValue = isNil(value) ? null : valueToOption(value);
|
const optionValue = isNil(value) ? null : valueToOption(value);
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import React from "react";
|
|||||||
|
|
||||||
import { flatMap, get, isArray, isNil, isObject } from "lodash";
|
import { flatMap, get, isArray, isNil, isObject } from "lodash";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import Select, {
|
import Select, {
|
||||||
ActionMeta,
|
|
||||||
ClassNamesConfig,
|
ClassNamesConfig,
|
||||||
GroupBase,
|
GroupBase,
|
||||||
OnChangeValue,
|
OnChangeValue,
|
||||||
@@ -12,7 +16,9 @@ import Select, {
|
|||||||
} from "react-select";
|
} from "react-select";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Field, FieldProps } from "./Field";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
|
|
||||||
export type BaseOption<T = string, D = unknown> = {
|
export type BaseOption<T = string, D = unknown> = {
|
||||||
value: T;
|
value: T;
|
||||||
@@ -54,7 +60,7 @@ export const classNames = <Option, IsMulti extends boolean = false>(
|
|||||||
"dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
"dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||||
),
|
),
|
||||||
groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase",
|
groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase",
|
||||||
option: ({ isFocused, isSelected, isDisabled }) =>
|
option: ({ isFocused, isDisabled }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
"px-3 py-2 hover:cursor-pointer",
|
"px-3 py-2 hover:cursor-pointer",
|
||||||
isDisabled && "opacity-50",
|
isDisabled && "opacity-50",
|
||||||
@@ -65,23 +71,30 @@ export const classNames = <Option, IsMulti extends boolean = false>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type SelectFieldProps<
|
export type SelectFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
IsMulti extends boolean = false,
|
IsMulti extends boolean = false,
|
||||||
T = string,
|
T = string,
|
||||||
D = unknown,
|
D = unknown,
|
||||||
> = FieldProps &
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
Omit<
|
Omit<
|
||||||
Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>,
|
Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>,
|
||||||
"onChange" | "value" | "classNames"
|
"onChange" | "value" | "classNames" | "name"
|
||||||
> & {
|
> & {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
};
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export const SelectField = <
|
export const SelectField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
IsMulti extends boolean = false,
|
IsMulti extends boolean = false,
|
||||||
T = string,
|
T = string,
|
||||||
D = unknown,
|
D = unknown,
|
||||||
>({
|
>({
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
className,
|
className,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
childrenWrapperClassName,
|
childrenWrapperClassName,
|
||||||
@@ -92,9 +105,9 @@ export const SelectField = <
|
|||||||
placeholder,
|
placeholder,
|
||||||
options,
|
options,
|
||||||
...selectProps
|
...selectProps
|
||||||
}: SelectFieldProps<IsMulti, T, D>) => {
|
}: SelectFieldProps<TFieldValues, TFieldName, IsMulti, T, D>) => {
|
||||||
const t = useTranslations("App");
|
const t = useTranslations("App");
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
@@ -118,6 +131,7 @@ export const SelectField = <
|
|||||||
<Field
|
<Field
|
||||||
{...{
|
{...{
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
className,
|
className,
|
||||||
label,
|
label,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
@@ -128,11 +142,10 @@ export const SelectField = <
|
|||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name={name}
|
name={name}
|
||||||
control={form.control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const onSelectChange = (
|
const onSelectChange = (
|
||||||
newValue: OnChangeValue<BaseOption<T, D>, IsMulti>,
|
newValue: OnChangeValue<BaseOption<T, D>, IsMulti>,
|
||||||
actionMeta: ActionMeta<BaseOption<T, D>>,
|
|
||||||
) => {
|
) => {
|
||||||
if (isNil(newValue)) onChange(null);
|
if (isNil(newValue)) onChange(null);
|
||||||
else if (isArray(newValue)) {
|
else if (isArray(newValue)) {
|
||||||
@@ -143,12 +156,12 @@ export const SelectField = <
|
|||||||
const optionValue = isNil(value)
|
const optionValue = isNil(value)
|
||||||
? null
|
? null
|
||||||
: isArray(value)
|
: isArray(value)
|
||||||
? value.map(valueToOption)
|
? // @ts-ignore - this is too complex for TS to understand
|
||||||
|
value.map(valueToOption)
|
||||||
: valueToOption(value);
|
: valueToOption(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isClearable
|
|
||||||
value={optionValue}
|
value={optionValue}
|
||||||
onChange={onSelectChange}
|
onChange={onSelectChange}
|
||||||
options={options}
|
options={options}
|
||||||
|
|||||||
@@ -1,14 +1,35 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Switch, SwitchProps } from "@headlessui/react";
|
import { Switch, SwitchProps } from "@headlessui/react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Field, FieldProps } from "./Field";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
|
|
||||||
|
type SwitchFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
|
Omit<SwitchProps<"button">, "name">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const SwitchField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: SwitchFieldProps<TFieldValues, TFieldName>,
|
||||||
|
) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
label,
|
label,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
@@ -19,12 +40,13 @@ export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
|
|||||||
|
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
{...{
|
{...{
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
label,
|
label,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -34,7 +56,7 @@ export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="flex h-[40px] items-center">
|
<div className="flex h-[40px] items-center">
|
||||||
@@ -49,7 +71,7 @@ export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
|
|||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<span className="ui-checked:translate-x-[19px] ui-not-checked:translate-x-[3px] inline-block h-[14px] w-[14px] transform rounded-full bg-white transition-transform" />
|
<span className="inline-block h-[14px] w-[14px] transform rounded-full bg-white transition-transform ui-checked:translate-x-[19px] ui-not-checked:translate-x-[3px]" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,23 +1,40 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Field, FieldProps } from "./Field";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
export const TextAreaField = (
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
props: FieldProps &
|
|
||||||
React.HTMLProps<HTMLTextAreaElement> & {
|
type TextAreaFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
|
Omit<React.HTMLProps<HTMLTextAreaElement>, "ref" | "name"> & {
|
||||||
handleChanged?: (props: { name: string }) => void;
|
handleChanged?: (props: { name: string }) => void;
|
||||||
startIcon?: React.ReactNode;
|
startIcon?: React.ReactNode;
|
||||||
},
|
required?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const TextAreaField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: TextAreaFieldProps<TFieldValues, TFieldName>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
label,
|
label,
|
||||||
required,
|
|
||||||
disabled,
|
|
||||||
className,
|
className,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
childrenWrapperClassName,
|
childrenWrapperClassName,
|
||||||
@@ -25,10 +42,12 @@ export const TextAreaField = (
|
|||||||
|
|
||||||
startIcon,
|
startIcon,
|
||||||
handleChanged,
|
handleChanged,
|
||||||
|
required,
|
||||||
|
|
||||||
|
disabled,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
@@ -40,8 +59,8 @@ export const TextAreaField = (
|
|||||||
<Field
|
<Field
|
||||||
{...{
|
{...{
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
label,
|
label,
|
||||||
required,
|
|
||||||
className,
|
className,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
childrenWrapperClassName,
|
childrenWrapperClassName,
|
||||||
@@ -50,7 +69,7 @@ export const TextAreaField = (
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Field, FieldProps } from "./Field";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
export const TextField = (
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
props: FieldProps &
|
|
||||||
React.HTMLProps<HTMLInputElement> & {
|
type TextFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
|
Omit<React.HTMLProps<HTMLInputElement>, "ref" | "name"> & {
|
||||||
handleChanged?: (props: { name: string }) => void;
|
handleChanged?: (props: { name: string }) => void;
|
||||||
startIcon?: React.ReactNode;
|
startIcon?: React.ReactNode;
|
||||||
},
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const TextField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: TextFieldProps<TFieldValues, TFieldName>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
type = "text",
|
type = "text",
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
@@ -29,7 +47,7 @@ export const TextField = (
|
|||||||
|
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const form = useFormContext();
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
@@ -57,6 +75,7 @@ export const TextField = (
|
|||||||
<Field
|
<Field
|
||||||
{...{
|
{...{
|
||||||
name,
|
name,
|
||||||
|
control,
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
className,
|
className,
|
||||||
@@ -67,7 +86,7 @@ export const TextField = (
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@
|
|||||||
"resultsTitle": "Résultats",
|
"resultsTitle": "Résultats",
|
||||||
"opponentEloPlaceholder": "Classement de l'adversaire",
|
"opponentEloPlaceholder": "Classement de l'adversaire",
|
||||||
"gameResultPlaceholder": "Résultat...",
|
"gameResultPlaceholder": "Résultat...",
|
||||||
"gameResult": "{result, select, win {Victoire} draw {Match nul} loss {Défaite} other {{result}}}",
|
"gameResult": "{result, select, win {Victoire} draw {Nul} loss {Défaite} other {{result}}}",
|
||||||
"addGameButton": "Ajouter un autre résultat de partie",
|
"addGameButton": "Ajouter un autre résultat de partie",
|
||||||
|
|
||||||
"resultsUrlLabel": "Collez un lien vers la page de résultats de la FFE",
|
"resultsUrlLabel": "Collez un lien vers la page de résultats de la FFE",
|
||||||
|
|||||||
@@ -67,3 +67,11 @@ export type ResponseMessage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ScrollableElement = Window | HTMLElement;
|
export type ScrollableElement = Window | HTMLElement;
|
||||||
|
|
||||||
|
// Prettify takes a type as its argument and returns a new type that has the same properties as the original type,
|
||||||
|
// but the properties are not intersected. This means that the new type is easier to read and understand.
|
||||||
|
// https://gist.github.com/palashmon/db68706d4f26d2dbf187e76409905399
|
||||||
|
|
||||||
|
export type Prettify<T> = {
|
||||||
|
[K in keyof T]: T[K];
|
||||||
|
} & {};
|
||||||
|
|||||||
Reference in New Issue
Block a user