mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Feat/new UI (#153)
* wip * feat: make forms functional * feat: finalize pages * chore: remove unused translations * feat: app context * feat: user context * feat: finalize username login * fix: use correct tab order in login form * feat: add oauth logic * chore: update readme and assets * chore: rename docs back to assets * feat: favicons * feat: custom background image config option * chore: add acknowledgements for background image * feat: sanitize redirect URL * feat: sanitize redirect URL on check * chore: fix dependabot config * refactor: bot suggestions * fix: correctly redirect to app and check for untrusted redirects * fix: run oauth auto redirect only when there is a redirect URI * refactor: change select color * fix: fix dockerfiles * fix: fix hook rendering * chore: remove translations cdn * chore: formatting * feat: validate api response against zod schema * fix: use axios error instead of generic error in login page
This commit is contained in:
		
							
								
								
									
										166
									
								
								frontend/src/components/ui/form.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								frontend/src/components/ui/form.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| import * as React from "react"; | ||||
| import * as LabelPrimitive from "@radix-ui/react-label"; | ||||
| import { Slot } from "@radix-ui/react-slot"; | ||||
| import { | ||||
|   Controller, | ||||
|   FormProvider, | ||||
|   useFormContext, | ||||
|   useFormState, | ||||
|   type ControllerProps, | ||||
|   type FieldPath, | ||||
|   type FieldValues, | ||||
| } from "react-hook-form"; | ||||
|  | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { Label } from "@/components/ui/label"; | ||||
|  | ||||
| const Form = FormProvider; | ||||
|  | ||||
| type FormFieldContextValue< | ||||
|   TFieldValues extends FieldValues = FieldValues, | ||||
|   TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, | ||||
| > = { | ||||
|   name: TName; | ||||
| }; | ||||
|  | ||||
| const FormFieldContext = React.createContext<FormFieldContextValue>( | ||||
|   {} as FormFieldContextValue, | ||||
| ); | ||||
|  | ||||
| const FormField = < | ||||
|   TFieldValues extends FieldValues = FieldValues, | ||||
|   TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, | ||||
| >({ | ||||
|   ...props | ||||
| }: ControllerProps<TFieldValues, TName>) => { | ||||
|   return ( | ||||
|     <FormFieldContext.Provider value={{ name: props.name }}> | ||||
|       <Controller {...props} /> | ||||
|     </FormFieldContext.Provider> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const useFormField = () => { | ||||
|   const fieldContext = React.useContext(FormFieldContext); | ||||
|   const itemContext = React.useContext(FormItemContext); | ||||
|   const { getFieldState } = useFormContext(); | ||||
|   const formState = useFormState({ name: fieldContext.name }); | ||||
|   const fieldState = getFieldState(fieldContext.name, formState); | ||||
|  | ||||
|   if (!fieldContext) { | ||||
|     throw new Error("useFormField should be used within <FormField>"); | ||||
|   } | ||||
|  | ||||
|   const { id } = itemContext; | ||||
|  | ||||
|   return { | ||||
|     id, | ||||
|     name: fieldContext.name, | ||||
|     formItemId: `${id}-form-item`, | ||||
|     formDescriptionId: `${id}-form-item-description`, | ||||
|     formMessageId: `${id}-form-item-message`, | ||||
|     ...fieldState, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| type FormItemContextValue = { | ||||
|   id: string; | ||||
| }; | ||||
|  | ||||
| const FormItemContext = React.createContext<FormItemContextValue>( | ||||
|   {} as FormItemContextValue, | ||||
| ); | ||||
|  | ||||
| function FormItem({ className, ...props }: React.ComponentProps<"div">) { | ||||
|   const id = React.useId(); | ||||
|  | ||||
|   return ( | ||||
|     <FormItemContext.Provider value={{ id }}> | ||||
|       <div | ||||
|         data-slot="form-item" | ||||
|         className={cn("grid gap-2", className)} | ||||
|         {...props} | ||||
|       /> | ||||
|     </FormItemContext.Provider> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function FormLabel({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<typeof LabelPrimitive.Root>) { | ||||
|   const { error, formItemId } = useFormField(); | ||||
|  | ||||
|   return ( | ||||
|     <Label | ||||
|       data-slot="form-label" | ||||
|       data-error={!!error} | ||||
|       className={cn("data-[error=true]:text-destructive", className)} | ||||
|       htmlFor={formItemId} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { | ||||
|   const { error, formItemId, formDescriptionId, formMessageId } = | ||||
|     useFormField(); | ||||
|  | ||||
|   return ( | ||||
|     <Slot | ||||
|       data-slot="form-control" | ||||
|       id={formItemId} | ||||
|       aria-describedby={ | ||||
|         !error | ||||
|           ? `${formDescriptionId}` | ||||
|           : `${formDescriptionId} ${formMessageId}` | ||||
|       } | ||||
|       aria-invalid={!!error} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function FormDescription({ className, ...props }: React.ComponentProps<"p">) { | ||||
|   const { formDescriptionId } = useFormField(); | ||||
|  | ||||
|   return ( | ||||
|     <p | ||||
|       data-slot="form-description" | ||||
|       id={formDescriptionId} | ||||
|       className={cn("text-muted-foreground text-sm", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function FormMessage({ className, ...props }: React.ComponentProps<"p">) { | ||||
|   const { error, formMessageId } = useFormField(); | ||||
|   const body = error ? String(error?.message ?? "") : props.children; | ||||
|  | ||||
|   if (!body) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <p | ||||
|       data-slot="form-message" | ||||
|       id={formMessageId} | ||||
|       className={cn("text-destructive text-sm", className)} | ||||
|       {...props} | ||||
|     > | ||||
|       {body} | ||||
|     </p> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export { | ||||
|   useFormField, | ||||
|   Form, | ||||
|   FormItem, | ||||
|   FormLabel, | ||||
|   FormControl, | ||||
|   FormDescription, | ||||
|   FormMessage, | ||||
|   FormField, | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros