This commit is contained in:
2025-02-21 15:25:49 -03:00
parent 142e33726d
commit df82a69af0
7 changed files with 129 additions and 47 deletions

View File

@@ -1,23 +1,45 @@
import React, { forwardRef, useContext, useId } from 'react' import type { ReactNode, ComponentPropsWithoutRef, ElementType } from 'react'
import {
createElement,
createContext,
forwardRef,
useContext,
useId,
} from 'react'
import { ExclamationCircleIcon } from '@heroicons/react/24/outline' import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
import { omit } from 'ramda' import { omit } from 'ramda'
import Loader from './loader' import { Loader } from './loader'
import clsx from 'clsx' import clsx from 'clsx'
const ControlContext = React.createContext({}) interface ControlContextProps {
id?: string
className?: string
[key: string]: unknown
}
function ControlProvider({ children, ...props }) { const ControlContext = createContext<ControlContextProps>({})
interface ControlProviderProps extends ControlContextProps {
children: ReactNode
}
function ControlProvider({ children, ...props }: ControlProviderProps) {
return ( return (
<ControlContext.Provider value={props}>{children}</ControlContext.Provider> <ControlContext.Provider value={props}>{children}</ControlContext.Provider>
) )
} }
function useControl(props) { function useControl<T extends ControlContextProps>(props?: T) {
const field = useContext(ControlContext) const field = useContext(ControlContext)
return { ...field, ...props } return { ...field, ...props } as T & ControlContextProps
} }
export const Control = forwardRef(function Control( interface ControlProps extends ComponentPropsWithoutRef<'div'> {
as?: ElementType
children?: ReactNode
}
export const Control = forwardRef<HTMLElement, ControlProps>(function Control(
{ as = 'div', children, ...props }, { as = 'div', children, ...props },
ref, ref,
) { ) {
@@ -26,23 +48,29 @@ export const Control = forwardRef(function Control(
return ( return (
<ControlProvider id={props?.id || id} {...props_}> <ControlProvider id={props?.id || id} {...props_}>
{React.createElement(as, { ref, ...props }, children)} {createElement(as, { ref, ...props }, children)}
</ControlProvider> </ControlProvider>
) )
}) })
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
as?: ElementType
isLoading?: boolean
children?: ReactNode
}
export function Button({ export function Button({
children, children,
as = 'button', as = 'button',
className, className,
isLoading = false, isLoading = false,
...props ...props
}) { }: ButtonProps) {
if (isLoading) { if (isLoading) {
props['disabled'] = isLoading props['disabled'] = isLoading
} }
return React.createElement( return createElement(
as, as,
{ {
className: clsx( className: clsx(
@@ -53,20 +81,23 @@ export function Button({
className, className,
), ),
...props, ...props,
}, } as React.HTMLAttributes<HTMLElement>,
<> <>
{isLoading && ( {isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-green-secondary rounded-xl"> <div className="absolute inset-0 flex items-center justify-center bg-green-secondary rounded-xl">
<Loader className="w-5 text-white" /> <Loader className="w-5 text-white" />
</div> </div>
)} )}
{children} {children}
</>, </>,
) )
} }
export function Label({ children, className, ...props }) { interface LabelProps extends ComponentPropsWithoutRef<'label'> {
children?: ReactNode
}
export function Label({ children, className, ...props }: LabelProps) {
const { id, htmlFor, className: _, ...field } = useControl(props) const { id, htmlFor, className: _, ...field } = useControl(props)
return ( return (
@@ -83,21 +114,25 @@ export function Label({ children, className, ...props }) {
) )
} }
export const Input = forwardRef(function Input( interface InputProps extends Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
as?: ElementType
size?: 'base'
children?: ReactNode
}
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ as = 'input', size = 'base', className, children, ...props }, { as = 'input', size = 'base', className, children, ...props },
ref, ref,
) { ) {
const { className: _, ...field } = useControl(props) const { className: _, ...field } = useControl(props)
const sizes = { base: 'h-12' } const sizes = { base: 'h-12' }
return React.createElement( return createElement(
as, as,
{ {
className: clsx( className: clsx(
'bg-white outline-none px-4 rounded-lg transition', 'bg-white outline-none px-4 rounded-lg transition',
'border border-green-light dark:border-gray-700 dark:bg-gray-800', 'border border-green-light dark:border-gray-700 dark:bg-gray-800',
'focus:ring-1 focus:border-green-secondary focus:ring-green-secondary focus:placeholder:text-transparent', 'focus:ring-1 focus:border-green-secondary focus:ring-green-secondary focus:placeholder:text-transparent',
// Tailwind's won't inherit focus behavior; you must define it explicitly for both modes.
'dark:focus:border-green-secondary', 'dark:focus:border-green-secondary',
'aria-[invalid=true]:border-red-400 aria-[invalid=true]:ring-red-400', 'aria-[invalid=true]:border-red-400 aria-[invalid=true]:ring-red-400',
'dark:aria-[invalid=true]:border-red-500 dark:aria-[invalid=true]:ring-red-500', 'dark:aria-[invalid=true]:border-red-500 dark:aria-[invalid=true]:ring-red-500',
@@ -108,34 +143,41 @@ export const Input = forwardRef(function Input(
), ),
ref, ref,
...field, ...field,
}, } as React.InputHTMLAttributes<HTMLInputElement>,
children, children,
) )
}) })
export const Checkbox = forwardRef(function Checkbox( interface CheckboxProps extends ComponentPropsWithoutRef<'input'> {
{ className, ...props }, className?: string
ref, }
) {
const { className: _, ...field } = useControl(props)
return ( export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
<input function Checkbox({ className, ...props }, ref) {
type="checkbox" const { className: _, ...field } = useControl(props)
className={clsx(
'text-green-secondary border border-gray-300',
'focus:ring-2 focus:border-green-secondary focus:ring-green-secondary focus:ring-offset-0 focus:ring-opacity-30',
'dark:border-gray-700 dark:bg-gray-800 focus:dark:border-security dark:checked:bg-green-secondary dark:checked:border-security dark:disabled:bg-gray-700',
'disabled:bg-gray-200 outline-none rounded transition',
className,
)}
ref={ref}
{...field}
/>
)
})
export function Error({ children }) { return (
<input
type="checkbox"
className={clsx(
'text-green-secondary border border-gray-300',
'focus:ring-2 focus:border-green-secondary focus:ring-green-secondary focus:ring-offset-0 focus:ring-opacity-30',
'dark:border-gray-700 dark:bg-gray-800 focus:dark:border-security dark:checked:bg-green-secondary dark:checked:border-security dark:disabled:bg-gray-700',
'disabled:bg-gray-200 outline-none rounded transition',
className,
)}
ref={ref}
{...field}
/>
)
},
)
interface ErrorProps {
children?: ReactNode
}
export function Error({ children }: ErrorProps) {
return ( return (
<div className="text-sm text-red-500 flex items-start gap-0.5"> <div className="text-sm text-red-500 flex items-start gap-0.5">
<ExclamationCircleIcon className="w-4 mt-[1.5px] flex-shrink-0" /> <ExclamationCircleIcon className="w-4 mt-[1.5px] flex-shrink-0" />

View File

@@ -1,6 +1,6 @@
import clsx from 'clsx' import clsx from 'clsx'
export default function Loader({ className, ...props }) { export function Loader({ className, ...props }: { className?: string }) {
return ( return (
<svg <svg
className={clsx('animate-spin', className)} className={clsx('animate-spin', className)}

View File

@@ -1,5 +1,3 @@
import React from 'react'
export function Regular({ ...props }) { export function Regular({ ...props }) {
return ( return (
<svg <svg

View File

@@ -7,7 +7,7 @@ import {
} from 'react' } from 'react'
import * as Auth from 'aws-amplify/auth' import * as Auth from 'aws-amplify/auth'
interface AuthContextType { export type AuthContextType = {
authUser: Auth.FetchUserAttributesOutput | null authUser: Auth.FetchUserAttributesOutput | null
signIn: ({ signIn: ({
username, username,
@@ -21,8 +21,14 @@ interface AuthContextType {
const AuthContext = createContext<AuthContextType | null>(null) const AuthContext = createContext<AuthContextType | null>(null)
export function useAuth() { export function useAuth(): AuthContextType {
return useContext(AuthContext) const ctx = useContext(AuthContext)
if (!ctx) {
throw new Error('useAuth must be used within an AuthProvider')
}
return ctx
} }
export function AuthProvider({ children }: { children: React.ReactNode }) { export function AuthProvider({ children }: { children: React.ReactNode }) {

View File

@@ -1,16 +1,23 @@
import type { SubmitHandler } from 'react-hook-form'
import type { AuthContextType } from '~/hooks/use-auth'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { useAuth } from '~/hooks/use-auth' import { useAuth } from '~/hooks/use-auth'
import { Card } from '~/layouts/auth' import { Card } from '~/layouts/auth'
import { Control, Label, Input, Button } from '~/components/form' import { Control, Label, Input, Button } from '~/components/form'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
type Input = {
username: string
password: string
}
export default function Signin() { export default function Signin() {
const navigate = useNavigate() const navigate = useNavigate()
const { register, handleSubmit, formState } = useForm() const { register, handleSubmit, formState } = useForm<Input>()
const { signIn } = useAuth() const { signIn } = useAuth()
const onSubmit = async (payload) => { const onSubmit: SubmitHandler<Input> = async (data) => {
const { nextStep } = await signIn(payload) const { nextStep } = await signIn(data)
return navigate('/') return navigate('/')
} }

View File

@@ -27,6 +27,7 @@
"@react-router/dev": "^7.2.0", "@react-router/dev": "^7.2.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/node": "^20", "@types/node": "^20",
"@types/ramda": "^0.30.2",
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1", "@types/react-dom": "^19.0.1",
"prettier": "^3.5.1", "prettier": "^3.5.1",
@@ -4752,6 +4753,16 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ramda": {
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.2.tgz",
"integrity": "sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"types-ramda": "^0.30.1"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.0.10", "version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
@@ -8610,6 +8621,13 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/ts-toolbelt": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tsconfck": { "node_modules/tsconfck": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz",
@@ -8663,6 +8681,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/types-ramda": {
"version": "0.30.1",
"resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.30.1.tgz",
"integrity": "sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ts-toolbelt": "^9.6.0"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",

View File

@@ -30,6 +30,7 @@
"@react-router/dev": "^7.2.0", "@react-router/dev": "^7.2.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/node": "^20", "@types/node": "^20",
"@types/ramda": "^0.30.2",
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1", "@types/react-dom": "^19.0.1",
"prettier": "^3.5.1", "prettier": "^3.5.1",