UI Design System: shadcn/ui + Tailwind 4 + layout shell #2

Merged
dmolinari merged 3 commits from feature/UI-DESIGN-SYSTEM into main 2026-04-14 14:45:08 +00:00
2 changed files with 115 additions and 75 deletions
Showing only changes of commit 5e1e979377 - Show all commits

View File

@@ -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"
name="username"
type="text"
required
autoComplete="username"
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"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="password" className="text-sm font-medium text-gray-700">
Contraseña
</label>
<input
id="password"
name="password"
type="password"
required
autoComplete="current-password"
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"
/>
</div>
{error && (
<p role="alert" className="text-sm text-red-600">
{error}
</p>
)}
<button
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'} <FormField
</button> control={form.control}
</form> name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Usuario</FormLabel>
<FormControl>
<Input
{...field}
type="text"
autoComplete="username"
disabled={isLoading}
placeholder="Ingresá tu usuario"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Contraseña</FormLabel>
<FormControl>
<Input
{...field}
type="password"
autoComplete="current-password"
disabled={isLoading}
placeholder="Ingresá tu contraseña"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? 'Ingresando...' : 'Ingresar'}
</Button>
</form>
</Form>
) )
} }

View File

@@ -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>
) )
} }