From 0d9082acb09949bfa193e02f64dfac5e9aa41fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Manzanero=20Dom=C3=ADnguez?= Date: Tue, 8 Oct 2024 20:26:30 -0600 Subject: [PATCH] finish fullstack app tutorial translation --- .../portfolio/es/build-a-fullstack-app.mdx | 16 - .../es/construye-una-app-fullstack.mdx | 1201 +++++++++++++++++ 2 files changed, 1201 insertions(+), 16 deletions(-) delete mode 100644 src/content/portfolio/es/build-a-fullstack-app.mdx create mode 100644 src/content/portfolio/es/construye-una-app-fullstack.mdx diff --git a/src/content/portfolio/es/build-a-fullstack-app.mdx b/src/content/portfolio/es/build-a-fullstack-app.mdx deleted file mode 100644 index 02fff3b..0000000 --- a/src/content/portfolio/es/build-a-fullstack-app.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Build a fullstack web app -description: - Build a fullstack web app using Next.js as meta-framework and PostgreSQL as - database. -tags: [Next.js, PostgreSQL, Prisma, Auth.js, tailwindcss, shadcn/ui] -image: /portfolio/build-a-fullstack-app/banner.png -imageCaption: - Banner with the tech stack used in this tutorial, Next.js, TailwindCSS, - shadcn/ui, Prisma, PostgreSQL and Auth.js. -date: 2024-1-18 -author: Juan Manzanero -rss: true ---- - -Hola diff --git a/src/content/portfolio/es/construye-una-app-fullstack.mdx b/src/content/portfolio/es/construye-una-app-fullstack.mdx new file mode 100644 index 0000000..d933b14 --- /dev/null +++ b/src/content/portfolio/es/construye-una-app-fullstack.mdx @@ -0,0 +1,1201 @@ +--- +title: Construye una App Fullstack +description: Construye una app fullstack usando Next.js como meta-framework y PostgreSQL como base de datos. +tags: [Next.js, PostgreSQL, Prisma, Auth.js, tailwindcss, shadcn/ui] +image: /portfolio/build-a-fullstack-app/banner.png +imageCaption: "Banner con el teck stack usado en este tutorial, Next.js, TailwindCSS, shadcn/ui, Prisma, PostgreSQL y Auth.js." +date: 2024-1-18 +author: Juan Manzanero +rss: true +--- + +![Banner con el teck stack usado en este tutorial, Next.js, TailwindCSS, shadcn/ui, Prisma, PostgreSQL y Auth.js.](@/assets/portfolio/build-a-fullstack-app/banner.png) +_Tech stack usado en este tutorial_ + +[GitHub repo](https://github.com/juancmandev/fullstack-app) + +# Contenido + +1. [Introducción](#1-introducción) +2. [Configuración inicial](#2-configuración-inicial) + 1. [Instalar shadcn/ui](#21-instalar-shadcnui) + 2. [Crear una base de datos PostgreSQL usando Docker](#22-crear-una-base-de-datos-postgresql-usando-docker) + 3. [Instalar Prisma](#23-instalar-prisma) + 4. [Configurar Auth.js](#24-configurar-authjs) +3. [Mejora tu UI](#3-mejora-tu-ui) +4. [Añade funcionalidad CRUD](#4-añade-funcionalidad-crud) +5. [Conclusión](#5-conclusión) + +## 1. Introducción + +En este tutorial, desarrollaremos una app fullstack con el siguiente tech stack: + +- [Next.js](https://nextjs.org/) como meta-framework +- [TailwindCSS](https://tailwindcss.com/) para estilos +- [shadcn/ui](https://ui.shadcn.com/) para componentes UI +- [Prisma](https://www.prisma.io/) como ORM +- [PostgreSQL](https://www.postgresql.org/) como base de datos +- [Auth.js](https://authjs.dev/) para autenticación +- [Docker](https://www.docker.com/) para crear una instancia de una base de datos PostgreSQL localmente + +Aprenderemos algunos de los fundamentos de este tech stack, como usar **server components** en Next.js, o crear **endpoints API** usando el **app router**. + +## 2. Configuración inicial + +Comencemos creando un nuevo proyecto Next.js, en tu `terminal` ejecuta: + +```bash title="Terminal" +npx create-next-app@latest +``` + +Asegúrate de marcar `Yes` en las siguientes opciones: + +- Would you like to use **TypeScript**? +- Would you like to use **ESLint**? +- Would you like to use **Tailwind CSS**? +- Would you like to use **'src/' directory**? +- Would you like to use **App Router**? (recommended) + +```bash title="Terminal" +> What is your project named? fullstack-app +> Would you like to use TypeScript? No / Yes +> Would you like to use ESLint? No / Yes +> Would you like to use Tailwind CSS? No / Yes +> Would you like to use `src/` directory? No / Yes +> Would you like to use App Router? (recommended) No / Yes +> Would you like to customize the default import alias (@/*)? No / Yes +> What import alias would you like configured? @/* +``` + +Espera a que la instalación de las dependencias termine, luego accede a la carpeta del proyecto: + +```bash title="Terminal" +cd fullstack-app +``` + +Abre tu editor de código de preferencia. + +### 2.1 Instalar shadcn/ui + +Estos componentes nos ayudarán mucho a construir la **UI** junto con TailwindCSS. + +Primero, inicializa shadcn/ui: + +```bash title="Terminal" +npx shadcn-ui@latest init +``` + +Asegúrate de configurar shadcn/ui de acuerdo a tu proyecto. + +Puedes revisar la [documentación de shadcn/ui](https://ui.shadcn.com/docs) para cada componente que puedas necesitar, cada componente se instala individualmente. + +### 2.2 Crear una base de datos PostgreSQL usando Docker + +Asegúrate de tener [Docker](https://www.docker.com/) instalado en tu máquina. + +Primero, necesitas descargar una imagen de PostgreSQL de Docker Hub: + +```bash title="Terminal" +docker pull postgres +``` + +Luego, crea un contenedor con la imagen: + +```bash title="Terminal" +docker run --name my-postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres +``` + +### 2.3 Instalar Prisma + +Instala Prisma usando tu gestor de dependencias, en este caso **npm**: + +```bash title="Terminal" +npm install prisma -D +``` + +Ahora inicializa Prisma: + +```bash title="Terminal" +npx prisma init +``` + +Se creará un directorio `./prisma` en la raíz de tu proyecto, con un archivo `schema.prisma`. + +Aquí crearás tus schemas. + +Agrega este modelo como ejemplo: + +```prisma title="prisma/schema.prisma" +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + email String @unique + name String? +} +``` + +Actualiza tu archivo .env con la siguiente URL: + +```env title=".env" +DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres?schema=public" +``` + +En esta URL está tu nombre de usuario (por defecto es postgres), tu contraseña (en este caso password), el host (por defecto es localhost), el puerto (por defecto es 5432), el nombre de la base de datos (por defecto es postgres) y el schema (por defecto es public). + +Crea tu primera migración para probar si Prisma puede conectarse a tu base de datos local: + +```bash title="Terminal" +npx prisma migrate dev --name init +``` + +Si todo está bien, verás un nuevo directorio `/migrations` con un nuevo archivo dentro. + +> Si tienes un error, asegúrate de poder conectarte a tu base de datos local. Elimina y crea el contenedor de nuevo si es necesario. + +### 2.4 Configurar Auth.js + +Agrega este modelo a tu `schema.prisma`: + +```prisma title="prisma/schema.prisma" +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} +``` + +Estos modelos son para **Auth.js**, ahora podemos instalarlo con el adaptador de Prisma: + +```bash title="Terminal" +npm install @prisma/client @auth/prisma-adapter +``` + +Instalaremos nodemailer también, ya que usaremos magic links para la autenticación: + +```bash title="Terminal" +npm install nodemailer -D +``` + +Ahora crea `src/utils/db.ts` e inicializa **prisma**: + +```ts title="src/utils/db.ts" +import { PrismaClient } from '@prisma/client'; + +const prismaClientSingleton = () => { + return new PrismaClient(); +}; + +declare global { + var prisma: undefined | ReturnType; +} + +const prisma = globalThis.prisma ?? prismaClientSingleton(); + +export default prisma; + +if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma; +``` + +Luego, crea `src/libs/auth.ts` para configurar Auth.js: + +```ts title="src/utils/auth.ts" +import type { NextAuthOptions } from 'next-auth'; +import { PrismaAdapter } from '@auth/prisma-adapter'; +import EmailProvider from 'next-auth/providers/email'; +import prisma from '@/libs/db'; +import { Adapter } from 'next-auth/adapters'; + +export const authOptions = { + adapter: PrismaAdapter(prisma) as Adapter, + providers: [ + EmailProvider({ + server: { + host: process.env.EMAIL_SERVER_HOST, + port: process.env.EMAIL_SERVER_PORT, + auth: { + user: process.env.EMAIL_SERVER_USER, + pass: process.env.EMAIL_SERVER_PASSWORD, + }, + }, + from: process.env.EMAIL_FROM, + }), + ], + callbacks: { + session: async ({ session, user }) => { + return { + ...session, + user: user, + }; + }, + }, +} satisfies NextAuthOptions; +``` + +Esta configuración es para usar un proveedor de email, para este proyecto usaremos [Resend](https://resend.com/). + +Crea una cuenta y luego obtén las siguientes credenciales en tu archivo .env: + +- EMAIL_SERVER_HOST: smtp.resend.com +- EMAIL_SERVER_PORT: 465 +- EMAIL_SERVER_USER: resend +- EMAIL_FROM: onboarding@resend(dot)dev +- EMAIL_SERVER_PASSWORD: yor api key + +Ahora, crea un archivo `src/pages/api/auth/[...nextauth].ts`: + +```ts title="src/app/api/auth/[...nextauth]/route.ts" +import { authOptions } from '@/libs/auth'; +import NextAuth from 'next-auth/next'; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; +``` + +Este archivo es para manejar la autenticación en nuestra app. + +Ahora puedes autenticar usuarios con un magic link enviado por email. + +Crea un archivo `src/app/auth/signin-form.tsx`: + +```tsx title="src/app/auth/signin-form.tsx" +'use client'; + +import { useState } from 'react'; +import { signIn } from 'next-auth/react'; + +export default function SigninForm() { + const [email, setEmail] = useState(null); + + async function handleSubmit() { + await signIn('email', { + email, + callbackUrl: `${window.location.origin}`, + }); + } + + return ( +
+
+ + setEmail(e.target.value)} + className='w-max p-1 border border-slate-400' + /> +
+ +
+ ); +} +``` + +Impórtalo en `src/app/auth/page.tsx`: + +Import it to your `src/app/auth/page.tsx` file: + +```tsx title="src/app/auth/page.tsx" +import { authOptions } from '@/libs/auth'; +import { getServerSession } from 'next-auth'; +import { redirect } from 'next/navigation'; +import SigninForm from './form'; + +export default async function Signin() { + const session = await getServerSession(authOptions); + + if (session) { + return redirect('/'); + } + + return ( + <> +

Sign in

+ + + + ); +} +``` + +Como puedes ver, puedes redirigir a los usuarios si no están autenticados obteniendo la sesión con **getServerSession**. + +## 3. Mejora tu UI + +Creemos una app para posts cortos. + +Primero, agrega algunos componentes de shadcn/ui y actualiza tus componentes, también crearemos nuevos componentes: + +```bash title="Terminal" +npx shadcn-ui@latest add button +``` + +```bash title="Terminal" +npx shadcn-ui@latest add dialog +``` + +```bash title="Terminal" +npx shadcn-ui@latest add input +``` + +```bash title="Terminal" +npx shadcn-ui@latest add textarea +``` + +```bash title="Terminal" +npx shadcn-ui@latest add form +``` + +```bash title="Terminal" +npx shadcn-ui@latest add label +``` + +```bash title="Terminal" +npx shadcn-ui@latest add sonner +``` + +Agregaremos las URL de los endpoints para estos componentes, pero más adelante. + +`src/app/auth/signin-form.tsx` + +Aquí actualizaremos la UI y agregaremos validación de formulario. + +```tsx title="src/app/auth/signin-form.tsx" +'use client'; + +import { signIn } from 'next-auth/react'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; + +const formSchema = z.object({ + email: z.string().email(), +}); + +export default function SigninForm() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + }, + }); + + async function onSubmit({ email }: z.infer) { + await signIn('email', { + email, + callbackUrl: `${window.location.origin}`, + }); + } + + return ( +
+ + ( + + Email + + + + + + )} + /> + + + + ); +} +``` + +`src/components/post/create.tsx` + +Actualiza los componentes de UI y agrega validación de formulario. + +```tsx title="src/components/post/create.tsx" +'use client'; + +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { SessionProps } from './types'; + +const formSchema = z.object({ + title: z.string().min(1).max(100), + content: z.string().min(1), +}); + +export default function CreatePost(props: SessionProps) { + const [open, setOpen] = useState(false); + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + content: '', + }, + }); + + async function onSubmit(values: z.infer) { + try { + const res = await fetch('/api/posts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...values, + authorId: props.session.user?.id, + }), + }); + const json = await res.json(); + + if (!res.ok) { + toast(json.message); + + return; + } + + toast('Post created!'); + form.reset(); + setOpen(false); + router.refresh(); + } catch (error) { + console.error(error); + } + } + + return ( + + + + + + + Create post + + Please do not post NSFW content. + + +
+ + ( + + Title + + + + + + )} + /> + ( + + Content + +