Oliver Servín

Oliver Servín

Desarrollador de software y emprendedor

Recreación de la barra de navegación de Netflix con React en Laravel Breeze.

Para los iconos que tendrá la barra de navegación utilizaremos Heroicons.

npm install @heroicons/react

Ahora modificaremos el layout para usuarios autenticados resources/js/Layouts/AuthenticatedLayout.jsx con lo siguiente:

import Dropdown from '@/Components/Dropdown'
import NavLink from '@/Components/NavLink'
import { BellIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
 
import { Link } from '@inertiajs/react'
import { useState } from 'react'
 
export default function Authenticated({ user, header, children }) {
const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false)
 
return (
<div className="min-h-screen bg-zinc-900">
<nav className="fixed z-40 w-full">
<div className="flex flex-row items-center bg-zinc-900 bg-opacity-90 px-4 py-6 transition duration-500 md:px-16">
<img src="/images/logo.svg" alt="Logo" className="h-4 lg:h-7" />
<div className="hidden lg:ml-8 lg:flex lg:flex-row lg:gap-7">
<NavLink href="/">Inicio</NavLink>
<NavLink href="/">Series</NavLink>
<NavLink href="/">Películas</NavLink>
<NavLink href="/">Agregados recientemente</NavLink>
<NavLink href="/">Mi lista</NavLink>
<NavLink href="/">Navegar por idiomas</NavLink>
</div>
<div className="ml-8 lg:hidden">
<Dropdown>
<Dropdown.Trigger>
<button type="button" className="flex items-center text-white">
<span className="text-white">Menú</span>
 
<svg
className="-me-0.5 ms-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="[REDACTED]"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M[REDACTED]a[REDACTED] 0L[REDACTED]l[REDACTED]a[REDACTED] 1.414l-4 4a[REDACTED].414 0l-4-4a[REDACTED].414z"
clipRule="evenodd"
/>
</svg>
</button>
</Dropdown.Trigger>
 
<Dropdown.Content align="left" contentClasses="py-1 bg-black border border-zinc-800">
<Dropdown.Link href="/" className="!text-base text-white hover:bg-zinc-900">
Inicio
</Dropdown.Link>
<Dropdown.Link href="/" className="!text-base text-white hover:bg-zinc-900">
Series
</Dropdown.Link>
<Dropdown.Link href="/" className="!text-base text-white hover:bg-zinc-900">
Películas
</Dropdown.Link>
<Dropdown.Link href="/" className="!text-base text-white hover:bg-zinc-900">
Agregados recientemente
</Dropdown.Link>
<Dropdown.Link href="/" className="!text-base text-white hover:bg-zinc-900">
Mi lista
</Dropdown.Link>
<Dropdown.Link href="/" className="!text-base text-white hover:bg-zinc-900">
Navegar por idiomas
</Dropdown.Link>
</Dropdown.Content>
</Dropdown>
</div>
<div className="ml-auto flex items-center gap-7">
<Link href="#" className="text-gray-200 hover:text-gray-300">
<MagnifyingGlassIcon className="size-6" />
</Link>
<Link href="#" className="text-gray-200 hover:text-gray-300">
<BellIcon className="size-6" />
</Link>
<Dropdown>
<Dropdown.Trigger>
<button type="button" className="flex items-center text-sm text-white">
<img src="/images/default-blue.png" alt="Avatar" className="h-8 w-8 rounded-md" />
 
<svg
className="-me-0.5 ms-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="[REDACTED]"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M[REDACTED]a[REDACTED] 0L[REDACTED]l[REDACTED]a[REDACTED] 1.414l-4 4a[REDACTED].414 0l-4-4a[REDACTED].414z"
clipRule="evenodd"
/>
</svg>
</button>
</Dropdown.Trigger>
 
<Dropdown.Content align="right" contentClasses="py-1 bg-black border border-zinc-800">
<div className="flex items-center gap-3 px-4 py-3 text-white">
<img src="/images/default-blue.png" alt="Avatar" className="size-6 rounded-md lg:size-10" />
{user.name}
</div>
<hr className="border-zinc-800" />
<Dropdown.Link
href={route('logout')}
method="post"
as="button"
className="!text-base text-white hover:bg-zinc-900"
>
Cerrar sesión
</Dropdown.Link>
</Dropdown.Content>
</Dropdown>
</div>
</div>
</nav>
<main className="pt-20">{children}</main>
</div>
)
}

También tendremos que modificar nuestro componente NavLink de Laravel Breeze resources/js/Components/NavLink.jsx.

import { Link } from '@inertiajs/react'
 
export default function NavLink({ className = '', children, ...props }) {
return (
<Link {...props} className={'cursor-pointer text-white transition hover:text-gray-300 ' + className}>
{children}
</Link>
)
}

Por último, actualizamos la página del dashboard resources/js/Pages/Dashboard.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import { Head } from '@inertiajs/react'
 
export default function Dashboard({ auth }) {
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="text-xl font-semibold leading-tight text-gray-800">Dashboard</h2>}
>
<Head title="Dashboard" />
 
<div className="text-center text-2xl text-white">Inicio</div>
</AuthenticatedLayout>
)
}

Netflix navbar

Creación de una pantalla de perfiles como la de Netflix.

Debemos agregar una nueva ruta en routes/web.php para los perfiles.

use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
 
Route::redirect('/', '/profiles');
 
Route::get('/profiles', function () {
return Inertia::render('Profiles');
})->middleware(['auth', 'verified'])->name('profiles');
 
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
 
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
 
require __DIR__.'/auth.php';

También creamos un nuevo layout para la pantalla de perfiles resources/js/Layouts/ProfilesLayout.jsx.

export default function Profiles({ children }) {
return <div className="flex min-h-screen items-center justify-center bg-neutral-900">{children}</div>
}

Y la página Inertia para los perfiles que mostrará el nombre del usuario y su imagen de perfil y que al hacer clic lo dirige a la página de dashboard.

import ProfilesLayout from '@/Layouts/ProfilesLayout'
import { Head, Link } from '@inertiajs/react'
 
export default function Profiles({ auth }) {
return (
<ProfilesLayout>
<Head title="Perfiles" />
 
<div className="flex flex-col">
<h1 className="text-center text-3xl text-white md:text-6xl">¿Quién está mirando?</h1>
<div className="mt-10 flex items-center justify-center gap-8">
<Link href={route('dashboard')}>
<div className="group mx-auto w-44 flex-row">
<div className="flex size-44 items-center justify-center overflow-hidden rounded-md border-2 border-transparent group-hover:cursor-pointer group-hover:border-white">
<img src="/images/default-blue.png" alt="Perfil" />
</div>
<div className="mt-4 text-center text-2xl text-gray-400 group-hover:text-white">{auth.user.name}</div>
</div>
</Link>
</div>
</div>
</ProfilesLayout>
)
}

Pantalla de perfiles

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.

Autenticación clonada

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.

registro

inicio de sesión

Instalación de Prettier en Inertia con React.

Para dar formato automático en nuestros proyectos Laravel con Inertia y React, podemos instalar Prettier.

npm install -D prettier-plugin-organize-imports prettier-plugin-tailwindcss

Y creamos el archivo prettier.config.mjs en la raíz de nuestro proyecto.

/** @type {import("prettier").Config} */
const config = {
semi: false,
singleQuote: true,
printWidth: 120,
trailingComma: 'es5',
tailwindFunctions: ['clsx', 'tw'],
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
}
 
export default config

Inicialización de un proyecto Laravel con Inertia y React.

Utilizando el instalador de Laravel podemos inicializar un nuevo proyecto.

laravel new

Nos hará algunas preguntas sobre nuestro proyecto y elegiremos utilizar Breeze con Inertia y React.

laravel new

También nos preguntará si queremos utilizar SQLite y ejecutar las migraciones.

sqlite

Después podemos ejecutar npm run dev en nuestro proyecto para ejecutar Vite y ver la landing page inicial.

laravel landing

Integrar Hono en Next.js.

Instalamos la dependencia de Hono con npm:

npm install hono

Agregamos una ruta del tipo catch-all para la API creando el archivo app/api/[[...route]]/route.ts.

Dentro del archivo route.ts inicializamos Hono y especificamos las constantes GET y POST para que Next.js maneje las rutas API pero a través de Hono.

import { Hono } from 'hono'
import { handle } from 'hono/vercel'
 
export const runtime = 'edge'
 
const app = new Hono().basePath('/api')
 
app.get('/hello', (c) => {
return c.json({
message: 'Hello Next.js!',
})
})
 
export const GET = handle(app)
export const POST = handle(app)

Ahora podemos ir al endpoint http://localhost:3000/api/hello para verificar que la integración esté correcta.

Hola Next.js

También podemos utilizar Zod para validar parámetros o cualquier otra entrada en Hono.

Instalamos Zod y Zod Validator para Hono:

npm install zod
npm install @hono/zod-validator

Ahora, para probar Zod, añadimos otro endpoint /hello/:test y utilizamos el middleware de zValidator y especificamos el tipo de parámetro que será test, por ejemplo string().

+import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
+import { z } from 'zod'
 
export const runtime = 'edge'
 
const app = new Hono().basePath('/api')
 
app
.get('/hello', (c) => {
return c.json({
message: '¡Hola Next.js!',
})
})
+ .get(
+ '/hello/:test',
+ zValidator(
+ 'param',
+ z.object({
+ test: z.string(),
+ })
+ ),
+ (c) => {
+ const { test } = c.req.valid('param')
+ 
+ return c.json({
+ message: `¡Hola Next.js!`,
+ test: test,
+ })
+ }
+ )
 
export const GET = handle(app)
export const POST = handle(app)

Para proteger nuestros endpoints de la API con Clerk, podemos hacerlo mediante un middleware.

Instalamos Clerk para Hono y el backend:

npm install @hono/clerk-auth @clerk/backend

Ahora, en nuestro route.ts para las APIs, agregamos el middleware de clerkMiddleware para validar que el usuario esté autorizado para utilizar nuestra API.

+import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
 
export const runtime = 'edge'
 
const app = new Hono().basePath('/api')
 
app
.get('/hello', clerkMiddleware(), (c) => {
const auth = getAuth(c)
 
if (!auth?.userId) {
return c.json({
message: 'Unauthorized',
}, 401)
}
 
return c.json({
message: '¡Hola Next.js!',
userId: auth.userId,
})
})
 
export const GET = handle(app)
export const POST = handle(app)

Si visitamos el endpoint http://localhost:3000/api/hello y no hemos iniciado sesión, obtendremos un error 401.

401 error

Y si iniciamos sesión, obtenemos nuestro mensaje de bienvenida y el id del usuario.

endpoint con sesión iniciada

Integrar Clerk en Next.js para autenticar usuarios.

Instalamos Clerk en nuestro proyecto.

npm install @clerk/nextjs

Nos registramos en Clerk y creamos un nuevo proyecto para nuestra aplicación Next.js.

Obtenemos las API Keys que nos proporciona Clerk y las pegamos en un archivo .env.local.

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
CLERK_SECRET_KEY=sk_test_***

Ahora creamos una ruta del tipo catch-all en Next.js creando un archivo app/(auth)/sign-up/[[...sign-up]]/page.tsx en la raíz de nuestro proyecto.

Lo mismo haremos para la ruta /sign-in creando el archivo app/(auth)/sign-in/[[...sign-in]]/page.tsx.

En el archivo app/(auth)/sign-up/[[...sign-up]]/page.tsx agregamos el componente SignUp de Clerk.

import { SignUp } from '@clerk/nextjs'
 
export default function Page() {
return <SignUp />
}

Y en el archivo app/(auth)/sign-in/[[...sign-in]]/page.tsx agregamos el componente SignIn de Clerk.

import { SignIn } from '@clerk/nextjs'
 
export default function Page() {
return <SignIn />
}

Ahora actualizamos nuestras variables en .env.local para agregar las rutas de inicio de sesión y registro.

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
CLERK_SECRET_KEY=sk_test_***
+NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
+NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

Actualizamos nuestro layout raíz para utilizar el componente ClerkProvider modificando el archivo app/layout.tsx.

import { ClerkProvider } from '@clerk/nextjs'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
 
const inter = Inter({ subsets: ['latin'] })
 
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<ClerkProvider afterSignOutUrl="/">
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
)
}

También tenemos que crear un archivo middleware.ts en la raíz de nuestro proyecto para comenzar a proteger rutas. Por ahora lo que haremos es proteger la ruta / con isProtectedRoute = createRouteMatcher(['/']).

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
 
const isProtectedRoute = createRouteMatcher(['/'])
 
export default clerkMiddleware((auth, request) => {
if (isProtectedRoute(request)) {
auth().protect()
}
 
return NextResponse.next()
})
 
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}

También movemos nuestra ruta raíz / para utilizar la agrupación de rutas de Next.js. Lo hacemos moviendo el archivo app/page.tsx a app/(dashboard)/page.tsx y cambiamos el contenido con lo siguiente:

export default function Home() {
return (
<div>
<p>Ruta protegida</p>
</div>
)
}

Ahora si accedemos a http://localhost:3000 nos debe redirigir a nuestra página de autenticación.

Podemos mejorar el diseño de nuestra página de autenticación.

Modificamos el archivo app/(auth)/sign-in/[[...sign-in]]/page.tsx con lo siguiente:

import { ClerkLoaded, ClerkLoading, SignIn } from '@clerk/nextjs'
import { Loader2 } from 'lucide-react'
import Image from 'next/image'
 
export default function Page() {
return (
<div className="grid min-h-screen grid-cols-1 lg:grid-cols-2">
<div className="h-full flex-col items-center justify-center px-4 lg:flex">
<div className="space-y-4 pt-16 text-center">
<h1 className="text-3xl font-bold text-[#2e2a47]">Bienvenido de nuevo</h1>
<p className="text-base text-[#7e8ca0]">Inicia sesión o crea una cuenta para volver a tu panel de control.</p>
</div>
<div className="mt-8 flex items-center justify-center">
<ClerkLoaded>
<SignIn />
</ClerkLoaded>
<ClerkLoading>
<Loader2 className="animate-spin text-muted-foreground" />
</ClerkLoading>
</div>
</div>
<div className="hidden h-full items-center justify-center bg-blue-600 lg:flex">
<Image src="/logo.svg" alt="Logo" width={100} height={100} />
</div>
</div>
)
}

El logo logo.svg es un logo de ejemplo que he tomado de Logoipsum y también hace uso de los componentes ClerkLoaded y ClerkLoading para mostrar un icono Loader2 de cargando, mientras se inicializa Clerk.

Lo mismo hacemos para la página de registro modificando el archivo app/(auth)/sign-up/[[...sign-up]]/page.tsx:

import { ClerkLoaded, ClerkLoading, SignUp } from '@clerk/nextjs'
import { Loader2 } from 'lucide-react'
import Image from 'next/image'
 
export default function Page() {
return (
<div className="grid min-h-screen grid-cols-1 lg:grid-cols-2">
<div className="h-full flex-col items-center justify-center px-4 lg:flex">
<div className="space-y-4 pt-16 text-center">
<h1 className="text-3xl font-bold text-[#2e2a47]">Bienvenido de nuevo</h1>
<p className="text-base text-[#7e8ca0]">Inicia sesión o crea una cuenta para volver a tu panel de control.</p>
</div>
<div className="mt-8 flex items-center justify-center">
<ClerkLoaded>
<SignUp />
</ClerkLoaded>
<ClerkLoading>
<Loader2 className="animate-spin text-muted-foreground" />
</ClerkLoading>
</div>
</div>
<div className="hidden h-full items-center justify-center bg-blue-600 lg:flex">
<Image src="/logo.svg" alt="Logo" width={100} height={100} />
</div>
</div>
)
}

Ahora tenemos nuestro sistema de autenticación implementado y podemos hacer un registro de prueba.

Una vez iniciada la sesión, podemos cerrarla utilizando el componente UserButton de Clerk.

Modificamos el archivo app/(dashboard)/page.tsx agregando lo siguiente:

import { UserButton } from '@clerk/nextjs'
 
export default function Home() {
return <UserButton />
}

Y nos mostrará un botón con opciones de nuestro perfil como cerrar la sesión.

UserButton

Inicialización de un proyecto Next.js con Shadcn UI.

Comenzamos con inicializar un nuevo proyecto Next.js:

npx create-next-app@latest

A todo responderemos sí, excepto al directorio src/ y al import alias.

Instalación de Next.js

Ahora, dentro del directorio de nuestro proyecto, inicializamos Next.js.

npm run dev

Si abrimos http://localhost:3000 en el navegador, podremos ver la página de inicio de ejemplo de Next.js.

Ahora instalamos Shadcn:

npx shadcn@latest init

En las opciones elegimos el estilo default, el color slate y utilizar variables css.

Ahora podemos añadir nuestro primer componente de Shadcn, que será el componente de botón.

npx shadcn@latest add button

Para probar el componente de botón que hemos añadido, podemos editar el archivo app/page.tsx, que está en la ruta raíz de nuestro proyecto, y añadir lo siguiente.

import { Button } from "@/components/ui/button";
 
export default function Home() {
return (
<Button>
Hola Mundo
</Button>
);
}

Y en el navegador podremos ver nuestro botón.

Botón Shadcn

Simple profile.

Un sencillo perfil de quién soy.

perfil

Interruptor.

Un interruptor para poder cambiar fácilmente el estado de una publicación. Útil cuando creo un borrador y después quiero hacerlo público.

En vez de crear un modal, simplemente estoy mostrando una alerta de confirmación nativa de cualquier navegador web.

Interruptor

Publicaciones públicas.

Agregue una opción para que pueda hacer públicas las publicaciones. Así también puedo utilizarlo para publicar borradores o notas privadas.

Publicación pública

Hola.

Primer post con esta idea de microblogging personal similar a X