This commit is contained in:
2025-02-21 14:56:02 -03:00
parent a8d0667b89
commit 634f422528
9 changed files with 335 additions and 39 deletions

View File

@@ -0,0 +1,145 @@
import React, { forwardRef, useContext, useId } from 'react'
import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
import { omit } from 'ramda'
import Loader from './loader'
import clsx from 'clsx'
const ControlContext = React.createContext({})
function ControlProvider({ children, ...props }) {
return (
<ControlContext.Provider value={props}>{children}</ControlContext.Provider>
)
}
function useControl(props) {
const field = useContext(ControlContext)
return { ...field, ...props }
}
export const Control = forwardRef(function Control(
{ as = 'div', children, ...props },
ref,
) {
const id = useId()
const props_ = omit(['id'], props)
return (
<ControlProvider id={props?.id || id} {...props_}>
{React.createElement(as, { ref, ...props }, children)}
</ControlProvider>
)
})
export function Button({
children,
as = 'button',
className,
isLoading = false,
...props
}) {
if (isLoading) {
props['disabled'] = isLoading
}
return React.createElement(
as,
{
className: clsx(
'font-medium text-green-primary rounded-lg bg-green-secondary hover:bg-green-support',
'h-12 px-4 relative',
'disabled:bg-green-secondary/50 disabled:pointer-events-none',
'transition',
className,
),
...props,
},
<>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-green-secondary rounded-xl">
<Loader className="w-5 text-white" />
</div>
)}
{children}
</>,
)
}
export function Label({ children, className, ...props }) {
const { id, htmlFor, className: _, ...field } = useControl(props)
return (
<label
className={clsx(
"black cursor-pointer aria-required:after:content-['*'] aria-required:after:ml-0.5 aria-required:after:text-red-500",
className,
)}
htmlFor={htmlFor ?? id}
{...field}
>
{children}
</label>
)
}
export const Input = forwardRef(function Input(
{ as = 'input', size = 'base', className, children, ...props },
ref,
) {
const { className: _, ...field } = useControl(props)
const sizes = { base: 'h-12' }
return React.createElement(
as,
{
className: clsx(
'bg-white outline-none px-4 rounded-lg transition',
'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',
// Tailwind's won't inherit focus behavior; you must define it explicitly for both modes.
'dark:focus:border-green-secondary',
'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',
'disabled:text-gray-400 disabled:border-gray-300 disabled:bg-gray-200',
'dark:disabled:text-gray-500 dark:disabled:bg-gray-700',
sizes?.[size],
className,
),
ref,
...field,
},
children,
)
})
export const Checkbox = forwardRef(function Checkbox(
{ className, ...props },
ref,
) {
const { className: _, ...field } = useControl(props)
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}
/>
)
})
export function Error({ children }) {
return (
<div className="text-sm text-red-500 flex items-start gap-0.5">
<ExclamationCircleIcon className="w-4 mt-[1.5px] flex-shrink-0" />
<div>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import clsx from 'clsx'
export default function Loader({ className, ...props }) {
return (
<svg
className={clsx('animate-spin', className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-50"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)
}

View File

@@ -16,7 +16,7 @@ interface AuthContextType {
username: string
password: string
}) => Promise<Auth.SignInOutput>
currentUser: () => Promise<Auth.FetchUserAttributesOutput | void>
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | null>(null)
@@ -29,17 +29,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [authUser, setAuthUser] =
useState<Auth.FetchUserAttributesOutput | null>(null)
const currentUser = useCallback(async () => {
try {
const currentUser = await Auth.fetchUserAttributes()
setAuthUser(currentUser)
return currentUser
} catch {
setAuthUser(null)
}
}, [])
const signIn = useCallback(
async ({
username,
@@ -65,16 +54,22 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
[],
)
const ctxValue = useMemo<AuthContextType>(
const signOut = useCallback(async (): Promise<void> => {
try {
return await Auth.signOut()
} catch {}
}, [])
const authContext = useMemo<AuthContextType>(
() => ({
authUser,
signIn,
currentUser,
signOut,
}),
[authUser, signIn, currentUser],
[authUser, signIn, signOut],
)
return (
<AuthContext.Provider value={ctxValue}>{children}</AuthContext.Provider>
<AuthContext.Provider value={authContext}>{children}</AuthContext.Provider>
)
}

View File

@@ -1,20 +1,47 @@
import { useNavigation } from 'react-router'
import { useNavigation, redirect, useNavigate } from 'react-router'
import { fetchAuthSession } from 'aws-amplify/auth'
import { Link } from 'react-router'
import { Outlet } from 'react-router'
import { useAuth } from '~/hooks/use-auth'
export async function clientLoader() {
const session = await fetchAuthSession()
if (!session?.tokens?.idToken) {
throw redirect('/auth')
}
}
export default function Layout() {
const navigation = useNavigation()
const navigate = useNavigate()
const { signOut } = useAuth()
const isNavigating = Boolean(navigation.location)
return (
<>
<ul>
<li>
<Link to="/orders">Pagamentos</Link>
<Link to="/users">Usuários</Link>
<Link to="/enrollments">Matrículas</Link>
</li>
</ul>
<nav>
<ul className="flex gap-1">
<li>
<Link to="/orders">Pagamentos</Link>
</li>
<li>
<Link to="/users">Usuários</Link>
</li>
<li>
<Link to="/enrollments">Matrículas</Link>
</li>
</ul>
<button
onClick={() => {
signOut()
navigate('/auth')
}}
>
Sair
</button>
</nav>
{isNavigating ? <>Loading...</> : <Outlet />}
</>

View File

@@ -49,3 +49,17 @@ export default function Auth() {
</>
)
}
export function Card({ children }: { children: React.ReactNode }) {
return (
<Container>
<div className="space-y-2.5 xl:space-y-5 rounded-xl bg-yellow-50 dark:bg-gray-700/60 p-4 lg:p-8 drop-shadow-sm shadow-sm">
{children}
</div>
</Container>
)
}
export function Container({ children }: { children: React.ReactNode }) {
return <div className="w-full 2xl:w-[26rem]">{children}</div>
}

View File

@@ -10,6 +10,7 @@ import {
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Amplify } from 'aws-amplify'
import { AuthProvider } from '~/hooks/use-auth'
import { Smallest as Logo } from '~/components/logo'
import amplifyconfig from './amplifyconfiguration.json'
Amplify.configure(amplifyconfig)
@@ -59,16 +60,7 @@ export default function App() {
export function HydrateFallback() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="w-10 h-10 border-4 border-gray-300 border-t-4 border-t-blue-500 rounded-full animate-spin" />
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
<Logo className="w-14 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
)
}

View File

@@ -1,8 +1,35 @@
import { useForm } from 'react-hook-form'
import { useAuth } from '~/hooks/use-auth'
import { Card } from '~/layouts/auth'
import { Control, Label, Input, Button } from '~/components/form'
import { useNavigate } from 'react-router'
export default function Signin() {
const navigate = useNavigate()
const { register, handleSubmit, formState } = useForm()
const { signIn } = useAuth()
const onSubmit = async (payload) => {
const { nextStep } = await signIn(payload)
return navigate('/')
}
return (
<form>
Username
<input />
</form>
<Card>
<form onSubmit={handleSubmit(onSubmit)}>
<Control>
<Label>Email ou CPF</Label>
<Input {...register('username')} />
</Control>
<Control>
<Label>Senha</Label>
<Input type="password" {...register('password')} />
</Control>
<Button type="submit" isLoading={formState.isSubmitting}>
Entrar
</Button>
</form>
</Card>
)
}