UI Design System: shadcn/ui + Tailwind 4 + layout shell #2
@@ -1,66 +1,90 @@
|
|||||||
import type { FormEvent } from 'react'
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
username: z.string().min(1, 'El usuario es requerido'),
|
||||||
|
password: z.string().min(1, 'La contraseña es requerida'),
|
||||||
|
})
|
||||||
|
|
||||||
|
type LoginFormValues = z.infer<typeof loginSchema>
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
onSubmit: (username: string, password: string) => void
|
onSubmit: (username: string, password: string) => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
error: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoginForm({ onSubmit, isLoading, error }: LoginFormProps) {
|
export function LoginForm({ onSubmit, isLoading }: LoginFormProps) {
|
||||||
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
const form = useForm<LoginFormValues>({
|
||||||
e.preventDefault()
|
resolver: zodResolver(loginSchema),
|
||||||
const form = e.currentTarget
|
defaultValues: { username: '', password: '' },
|
||||||
const data = new FormData(form)
|
})
|
||||||
const username = data.get('username') as string
|
|
||||||
const password = data.get('password') as string
|
function handleSubmit(values: LoginFormValues) {
|
||||||
onSubmit(username, password)
|
onSubmit(values.username, values.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full max-w-sm">
|
<Form {...form}>
|
||||||
<div className="flex flex-col gap-1">
|
<form
|
||||||
<label htmlFor="username" className="text-sm font-medium text-gray-700">
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
Usuario
|
className="space-y-4"
|
||||||
</label>
|
noValidate
|
||||||
<input
|
>
|
||||||
id="username"
|
<FormField
|
||||||
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Usuario</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
placeholder="Ingresá tu usuario"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<FormField
|
||||||
<label htmlFor="password" className="text-sm font-medium text-gray-700">
|
control={form.control}
|
||||||
Contraseña
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
required
|
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
placeholder="Ingresá tu contraseña"
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
{error && (
|
</FormItem>
|
||||||
<p role="alert" className="text-sm text-red-600">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<Button type="submit" disabled={isLoading} className="w-full">
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Ingresando...' : 'Ingresar'}
|
{isLoading ? 'Ingresando...' : 'Ingresar'}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { isAxiosError } from 'axios'
|
||||||
|
import { AlertCircle } from 'lucide-react'
|
||||||
import { useLogin } from '../hooks/useLogin'
|
import { useLogin } from '../hooks/useLogin'
|
||||||
import { LoginForm } from '../components/LoginForm'
|
import { LoginForm } from '../components/LoginForm'
|
||||||
import { isAxiosError } from 'axios'
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
|
||||||
|
function resolveErrorMessage(err: unknown): string | null {
|
||||||
|
if (!err) return null
|
||||||
|
if (isAxiosError(err) && err.response?.data) {
|
||||||
|
const data = err.response.data as { error?: string }
|
||||||
|
return data.error ?? 'Error al iniciar sesión'
|
||||||
|
}
|
||||||
|
return 'Error al iniciar sesión'
|
||||||
|
}
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -18,27 +36,25 @@ export function LoginPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveErrorMessage(err: unknown): string | null {
|
const errorMessage = resolveErrorMessage(error)
|
||||||
if (!err) return null
|
|
||||||
if (isAxiosError(err) && err.response?.data) {
|
|
||||||
const data = err.response.data as { error?: string }
|
|
||||||
return data.error ?? 'Error al iniciar sesión'
|
|
||||||
}
|
|
||||||
return 'Error al iniciar sesión'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50">
|
<Card className="w-full max-w-sm">
|
||||||
<div className="rounded-lg bg-white p-8 shadow-md w-full max-w-sm">
|
<CardHeader className="space-y-1">
|
||||||
<h1 className="mb-6 text-center text-2xl font-semibold text-gray-900">
|
<CardTitle className="text-2xl text-center">SIG-CM 2.0</CardTitle>
|
||||||
SIG-CM2
|
<CardDescription className="text-center">
|
||||||
</h1>
|
Iniciar sesión
|
||||||
<LoginForm
|
</CardDescription>
|
||||||
onSubmit={handleSubmit}
|
</CardHeader>
|
||||||
isLoading={isPending}
|
<CardContent className="space-y-4">
|
||||||
error={resolveErrorMessage(error)}
|
{errorMessage && (
|
||||||
/>
|
<Alert variant="destructive">
|
||||||
</div>
|
<AlertCircle className="h-4 w-4" />
|
||||||
</div>
|
<AlertDescription>{errorMessage}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<LoginForm onSubmit={handleSubmit} isLoading={isPending} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user