Creación de pantallas de autenticación como en Netflix con Laravel Breeze, Inertia y React.
Veamos cómo replicar las páginas de autenticación de Netflix en Laravel Breeze.
Comenzamos con reemplazar el archivo del layout para guest resources/js/Layouts/GuestLayout.jsx
con el siguiente contenido.
import { Link } from '@inertiajs/react' export default function Guest({ children }) { return ( <div className="relative min-h-screen w-full bg-[url('/images/hero.jpg')] bg-cover bg-fixed bg-center bg-no-repeat"> <div className="absolute inset-0 bg-black lg:opacity-50"></div> <div className="relative z-10"> <nav className="px-12 py-5"> <Link href="/"> <img src="/images/logo.svg" alt="logo" className="h-12" /> </Link> </nav> <div className="flex justify-center"> <div className="mt-2 w-full self-center rounded-md bg-black bg-opacity-70 p-16 lg:w-2/5 lg:max-w-md"> {children} </div> </div> </div> </div> )}
Ahora, descargamos los archivos https://github.com/oliverservin/netflix-clone/blob/main/public/images/hero.jpg
y https://github.com/oliverservin/netflix-clone/blob/main/public/images/logo.svg
y los guardamos en nuestro proyecto en la carpeta public/images
.
Ahora cambiamos el contenido de nuestra página de registro resources/js/Pages/Auth/Register.jsx
con lo siguiente.
import InputError from '@/Components/InputError'import PrimaryButton from '@/Components/PrimaryButton'import TextInput from '@/Components/TextInput'import GuestLayout from '@/Layouts/GuestLayout'import { Head, Link, useForm } from '@inertiajs/react' export default function Register() { const { data, setData, post, processing, errors, reset } = useForm({ name: '', email: '', password: '', password_confirmation: '', }) const submit = (e) => { e.preventDefault() post(route('register'), { onFinish: () => reset('password', 'password_confirmation'), }) } return ( <GuestLayout> <Head title="Registrarse" /> <h2 className="mb-8 text-4xl font-semibold text-white">Registrarse</h2> <form onSubmit={submit}> <div> <TextInput id="name" label="Nombre" name="name" value={data.name} className="mt-1 block w-full" autoComplete="name" isFocused={true} onChange={(e) => setData('name', e.target.value)} required /> <InputError message={errors.name} className="mt-2" /> </div> <div className="mt-4"> <TextInput id="email" label="Email" type="email" name="email" value={data.email} className="mt-1 block w-full" autoComplete="username" onChange={(e) => setData('email', e.target.value)} required /> <InputError message={errors.email} className="mt-2" /> </div> <div className="mt-4"> <TextInput id="password" label="Contraseña" type="password" name="password" value={data.password} className="mt-1 block w-full" autoComplete="new-password" onChange={(e) => setData('password', e.target.value)} required /> <InputError message={errors.password} className="mt-2" /> </div> <div className="mt-4"> <TextInput id="password_confirmation" label="Confirmar Contraseña" type="password" name="password_confirmation" value={data.password_confirmation} className="mt-1 block w-full" autoComplete="new-password" onChange={(e) => setData('password_confirmation', e.target.value)} required /> <InputError message={errors.password_confirmation} className="mt-2" /> </div> <div className="mt-4"> <PrimaryButton className="w-full justify-center" disabled={processing}> Registrarse </PrimaryButton> </div> <div className="mt-12 text-center"> <p className="text-sm text-zinc-500"> ¿Ya tienes una cuenta?{' '} <Link href={route('login')} className="text-sm text-zinc-400 underline hover:text-white focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:ring-offset-2" > Iniciar Sesión </Link> </p> </div> </form> </GuestLayout> )}
Y también nuestra página de inicio de sesión resources/js/Pages/Auth/Login.jsx
con lo siguiente:
import Checkbox from '@/Components/Checkbox'import InputError from '@/Components/InputError'import PrimaryButton from '@/Components/PrimaryButton'import TextInput from '@/Components/TextInput'import GuestLayout from '@/Layouts/GuestLayout'import { Head, Link, useForm } from '@inertiajs/react' export default function Login({ status, canResetPassword }) { const { data, setData, post, processing, errors, reset } = useForm({ email: '', password: '', remember: false, }) const submit = (e) => { e.preventDefault() post(route('login'), { onFinish: () => reset('password'), }) } return ( <GuestLayout> <Head title="Iniciar Sesión" /> {status && <div className="mb-4 text-sm font-medium text-green-600">{status}</div>} <h2 className="mb-8 text-4xl font-semibold text-white">Iniciar Sesión</h2> <form onSubmit={submit}> <div> <TextInput id="email" label="Email" type="email" name="email" value={data.email} className="mt-1 block w-full" autoComplete="username" isFocused={true} onChange={(e) => setData('email', e.target.value)} /> <InputError message={errors.email} className="mt-2" /> </div> <div className="mt-4"> <TextInput id="password" label="Contraseña" type="password" name="password" value={data.password} className="mt-1 block w-full" autoComplete="current-password" onChange={(e) => setData('password', e.target.value)} /> <InputError message={errors.password} className="mt-2" /> </div> <div className="mt-4"> <label className="flex items-center"> <Checkbox name="remember" checked={data.remember} onChange={(e) => setData('remember', e.target.checked)} /> <span className="ms-2 text-sm text-zinc-400">Recuérdame</span> </label> </div> <div className="mt-4"> <PrimaryButton className="w-full justify-center" disabled={processing}> Iniciar Sesión </PrimaryButton> </div> <div className="mt-12"> <p className="text-sm text-zinc-500"> ¿Es tu primera vez en Netflix?{' '} <Link href={route('register')} className="text-sm text-zinc-400 underline hover:text-white focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:ring-offset-2" > Crea una cuenta </Link> </p> </div> {canResetPassword && ( <div className="mt-4 text-center"> <Link href={route('password.request')} className="text-sm text-zinc-400 underline hover:text-white focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:ring-offset-2" > ¿Olvidaste tu contraseña? </Link> </div> )} </form> </GuestLayout> )}
Ahora modificaremos nuestros componentes TextInput
, PrimaryButton
y el de Checkbox
.
En el archivo resources/js/Components/TextInput.jsx
colocamos lo siguiente:
import { forwardRef, useEffect, useRef } from 'react' export default forwardRef(function TextInput({ type = 'text', className = '', isFocused = false, ...props }, ref) { const input = ref ? ref : useRef() useEffect(() => { if (isFocused) { input.current.focus() } }, []) return ( <div className="relative"> <input {...props} type={type} className={ 'peer appearance-none rounded-md border-0 bg-neutral-700 px-6 pb-1 pt-6 text-base text-white focus:outline-none focus:ring-0 focus-visible:border-0' + className } ref={input} placeholder=" " /> <label htmlFor={props.id} className="absolute left-6 top-4 z-10 origin-[0] -translate-y-3 scale-75 transform text-base text-zinc-400 duration-150 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-3 peer-focus:scale-75" > {props.label} </label> </div> )})
En el archivo resources/js/Components/PrimaryButton.jsx
colocamos lo siguiente:
export default function PrimaryButton({ className = '', disabled, children, ...props }) { return ( <button {...props} className={ `inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 font-semibold text-white transition duration-150 ease-in-out hover:bg-red-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900 ${ disabled && 'opacity-25' } ` + className } disabled={disabled} > {children} </button> )}
Finalmente en el archivo resources/js/Components/Checkbox.jsx
colocamos lo siguiente:
export default function Checkbox({ className = '', ...props }) { return ( <input {...props} type="checkbox" className={'rounded border-0 bg-neutral-700 text-neutral-600 shadow-sm focus:ring-neutral-500 ' + className} /> )}
Ahora si visitamos nuestras páginas de autenticación se mostrarán de la siguiente manera.