feat: add campaign configuration controls
This commit is contained in:
218
components/ui/form.tsx
Normal file
218
components/ui/form.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
type SubmitHandler,
|
||||
type UseFormReturn,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type FormProps<TFieldValues extends FieldValues> = Omit<
|
||||
React.FormHTMLAttributes<HTMLFormElement>,
|
||||
"onSubmit" | "children"
|
||||
> & {
|
||||
form: UseFormReturn<TFieldValues>;
|
||||
onSubmit: SubmitHandler<TFieldValues>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<{ id: string } | null>(null);
|
||||
|
||||
type FormFieldContextValue = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
|
||||
|
||||
const Form = <TFieldValues extends FieldValues>({
|
||||
form,
|
||||
onSubmit,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: FormProps<TFieldValues>) => {
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
className={cn("w-full space-y-4", className)}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const { getFieldState, formState, control } = useFormContext();
|
||||
|
||||
if (!itemContext || !fieldContext) {
|
||||
throw new Error("useFormField must be used within a <FormField>.");
|
||||
}
|
||||
|
||||
return {
|
||||
control,
|
||||
id: itemContext.id,
|
||||
name: fieldContext.name,
|
||||
...getFieldState(fieldContext.name, formState),
|
||||
};
|
||||
};
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues,
|
||||
TName extends FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: Omit<ControllerProps<TFieldValues, TName>, "render"> & {
|
||||
render: ControllerProps<TFieldValues, TName>["render"];
|
||||
}) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: String(props.name) }}>
|
||||
<Controller
|
||||
control={props.control}
|
||||
name={props.name}
|
||||
render={props.render}
|
||||
/>
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("grid gap-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
HTMLLabelElement,
|
||||
React.LabelHTMLAttributes<HTMLLabelElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, id } = useFormField();
|
||||
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
htmlFor={props.htmlFor ?? id}
|
||||
className={cn("text-sm leading-none font-medium", className)}
|
||||
style={error ? { color: "var(--destructive)" } : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const getFormControlAriaDescribedBy = (fieldId: string, hasError: boolean) => {
|
||||
const descriptionId = `${fieldId}-description`;
|
||||
const messageId = `${fieldId}-message`;
|
||||
|
||||
if (hasError) {
|
||||
return `${descriptionId} ${messageId}`;
|
||||
}
|
||||
|
||||
return descriptionId;
|
||||
};
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.HTMLAttributes<HTMLElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { id, error } = useFormField();
|
||||
const controlId = props.id ?? id;
|
||||
|
||||
const control = React.Children.only(children);
|
||||
|
||||
if (!React.isValidElement(control)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typedControl = control as React.ReactElement<
|
||||
React.ClassAttributes<unknown> & Record<string, unknown>
|
||||
>;
|
||||
|
||||
const controlClassName = (typedControl.props as { className?: string })
|
||||
.className;
|
||||
|
||||
return React.cloneElement(typedControl, {
|
||||
id: controlId,
|
||||
ref: ref,
|
||||
className: cn("relative", className, controlClassName),
|
||||
...props,
|
||||
"aria-invalid": error ? "true" : "false",
|
||||
"aria-describedby": getFormControlAriaDescribedBy(
|
||||
controlId,
|
||||
!!error?.message,
|
||||
),
|
||||
"aria-errormessage": error?.message ? `${controlId}-message` : undefined,
|
||||
});
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { id } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={`${id}-description`}
|
||||
className={cn("text-xs leading-5 text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, id } = useFormField();
|
||||
|
||||
if (!error?.message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={`${id}-message`}
|
||||
className={cn("text-xs text-destructive", className)}
|
||||
role="alert"
|
||||
{...props}
|
||||
>
|
||||
{typeof error.message === "string" ? error.message : String(error.message)}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
Form,
|
||||
useFormField,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
};
|
||||
Reference in New Issue
Block a user