Vamos começar criando o projeto com Next.js 13
Logo ao abrir o projeto com o VS Code podemos ver que a estrutura do diretório “app” é bem diferente da tradicional. Trabalhar as interfaces agora é bem mais fácil e organizado. Vamos começar criando uma nova página. Crie a pasta “about” e dentro dela o arquivo “page.js”:
Obs.: repare também como é fácil manipular as metatags de uma página.
export const metadata = {
title: "Sobre | Front-end Insights",
description: "A Front-End Insights é uma newsletter semanal dedicada a fornecer conhecimentos valiosos sobre o mundo do desenvolvimento front-end.",
openGraph: {
title: "Sobre | Front-end Insights",
description: "A Front-End Insights é uma newsletter semanal dedicada a fornecer conhecimentos valiosos sobre o mundo do desenvolvimento front-end.",
}
}
export default function About() {
const h1 = "mt-10 mb-6 text-2xl font-bold"
const h2 = "mt-8 mb-4 text-xl font-bold"
const p = "mt-4 mb-2"
return (
<main>
<h1 className={h1}>Sobre a Newsletter "Front-End Insights"</h1>
<h2 className={h2}>O que é a Front-End Insights?</h2>
<p className={p}>A Front-End Insights é uma newsletter semanal dedicada a fornecer conhecimentos valiosos sobre o mundo do desenvolvimento front-end. Se você é um entusiasta, estudante, profissional ou simplesmente tem interesse nessa área, esta newsletter é perfeita para você! Nossa missão é trazer as últimas tendências, dicas e truques do front-end diretamente para a sua caixa de entrada, ajudando você a se manter atualizado e aprimorar suas habilidades.</p>
<h2 className={h2}>Por que assinar a Front-End Insights?</h2>
<ol className="list-decimal pl-4 [&>li]:mt-2">
<li><strong>Conhecimento atualizado:</strong> Nossa equipe de especialistas está sempre atenta às últimas novidades e tendências no mundo do front-end. Ao assinar a Front-End Insights, você receberá informações atualizadas sobre frameworks, bibliotecas, práticas recomendadas e muito mais.</li>
<li><strong>Dicas e truques exclusivos:</strong> Queremos ajudar você a se destacar como desenvolvedor front-end. Através da Front-End Insights, compartilhamos dicas e truques exclusivos que podem impulsionar suas habilidades e melhorar sua eficiência no trabalho.</li>
<li><strong>Recursos selecionados:</strong> Navegar pela imensidão de recursos disponíveis para desenvolvedores front-end pode ser esmagador. Na Front-End Insights, selecionamos cuidadosamente os melhores recursos, como tutoriais, artigos, vídeos e ferramentas, para facilitar sua jornada de aprendizado.</li>
</ol>
<h2 className={h2}>Quem está por trás da Front-End Insights?</h2>
<p className={p}>A Front-End Insights é criada e cuidadosamente curada por uma equipe de especialistas apaixonados por desenvolvimento front-end. Nossos colaboradores têm ampla experiência no setor e estão ansiosos para compartilhar seus conhecimentos e insights com você. Juntos, buscamos fornecer um conteúdo relevante e de qualidade para ajudar você a se destacar no mundo do front-end.</p>
<p className={p}>Não perca a oportunidade de se juntar a uma comunidade de desenvolvedores front-end ávidos por aprender, crescer e se inspirar. Inscreva-se agora mesmo na Front-End Insights e mergulhe em um mundo repleto de descobertas front-end emocionantes!</p>
</main>
)
}
Agora vamos editar o layout da página. O Next.js agora usa nomes especiais para os arquivos, então o layout é editável através do arquivo “layout.js”. O projeto padrão já inclui um desses na raiz do “app”. Modifique-o da seguinte forma:
Obs.: repare que o layout se aplicou também à página “about”, porque ele funciona com base no aninhamento dos diretórios, ou seja, se criarmos um “layout.js” no diretório “about” ele irá se aplicar a página “about” e a todas as suas subpáginas juntamente com o layout raiz, mas não a outras páginas de mesmo nível na hierarquia.
Obs².: repare também que aqui também estamos usando o novo “next/font” para usar a fonte Poppins de forma bem fácil e otimizada.
import Link from 'next/link'
import './globals.css'
import { Poppins } from 'next/font/google'
const poppins = Poppins({ weight: ['400', '700'], subsets: ['latin'] })
export const metadata = {
title: 'Front-End Insights',
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
return (
<html lang="pt-BR">
<body className={poppins.className}>
<div className="flex min-h-screen flex-col items-center justify-between max-w-5xl mx-auto">
<header className="flex items-center justify-between w-full py-10">
<span className="text-2xl font-black cursor-default">INSIGHTS</span>
<nav className="flex gap-10">
<Link href="/">início</Link>
<Link href="/about">sobre</Link>
<Link href="/subscribers">inscritos</Link>
</nav>
</header>
{children}
<footer className="p-10">
<p>Feito com Next.js 13.4</p>
</footer>
</div>
</body>
</html>
)
}
Agora vamos atualizar a página principal para mostrar a tela de inscrição da nossa newsletter:
import SubscribeForm from "@/components/SubscribeForm";
export default function Home() {
return (
<main className="text-center">
<h1 className="text-4xl">
Mergulhe no mundo do Front-End: Receba as últimas tendências, dicas e truques toda semana!
</h1>
<p className="mt-8 text-lg">
Inscreva-se em nossa newsletter semanal para se manter atualizado sobre os avanços mais recentes no desenvolvimento front-end, descubra novas ferramentas, aprimore suas habilidades e aprofunde seu conhecimento nesta área dinâmica!
</p>
<SubscribeForm />
</main>
)
}
E vamos criar a pasta “components” e o componente “SubscribeForm” para usá-lo na página inicial:
import { useState } from "react"
export default function SubscribeForm() {
const [email, setEmail] = useState("")
const handleSubmit = async (e) => {
e.preventDefault()
setEmail("")
alert("Email cadastrado com sucesso!")
}
return (
<form
className="flex justify-center gap-4 p-4"
onSubmit={handleSubmit}
>
<input
type="email"
name="email"
id="email"
placeholder="Seu e-mail principal"
className="bg-slate-800 p-3 rounded"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button className="bg-sky-700 p-3 rounded">
Se inscrever
</button>
</form>
)
}
Porém, ao tentar acessar a página veremos que temos um erro no código. Esse erro acontece por conta dos React Server Components, que não permitem o uso de código client-side, como é o caso do nosso useState(). Para resolver isso é simples, basta indicar que não queremos que este seja um server component acrescentado o “use client” no topo do arquivo.
Agora vamos ver outro ponto importante do Next.js, as clássicas rotas de API. Na versão 13 com o diretório “app” não temos mais uma pasta específica de “api”, ao invés disso podemos criar uma rota customizada em qualquer lugar do diretório “app”. Como sempre, usamos um nome especial, nesse caso “route.js”. Só é importante se atentar para o fato de que não podemos misturar uma “page” e uma “route” no mesmo diretório (então ainda pode ser uma boa usar um diretório “api”). No arquivo “route” podemos criar funções para cada método HTTP aceito pela rota:
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Olá, mundo!" })
}
O legal do novo formato usado pelo diretório “app” é que podemos usar nosso código backend com mais flexibilidade também. Vamos começar fazendo um rápido setup de um bacno de dados MySQL:
CREATE DATABASE nextjs_13;
use nextjs_13
CREATE TABLE Subscribers (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE USER 'nextjs'@'localhost' IDENTIFIED BY 'nextjs';
GRANT ALL PRIVILEGES ON nextjs_13.* TO 'nextjs'@'localhost';
FLUSH PRIVILEGES;
Vamos instalar o driver de conexão com o MySQL, o “mysql2”:
npm install mysql2
Agora vamos criar uma rota com um método POST para salvar os inscritos da nossa newsletter. Crie uma pasta “subscribers” dentro de “api” e crie o arquivo “route.js” nela:
import { NextResponse } from "next/server"
import mysql from "mysql2/promise"
export async function POST(request) {
const body = await request.json()
try {
const connection = await mysql.createConnection("mysql://nextjs:nextjs@localhost:3306/nextjs_tutorial")
await connection.query("INSERT INTO Subscribers (email) VALUES (?)", [body.email])
connection.end()
return NextResponse.json({ created: true })
} catch (error) {
return NextResponse.json({ created: false, error: error.message }, { status: 400 })
}
}
E agora podemos chamar esse endpoint pelo lado do cliente no componente SubscribeForm:
"use client"
import { useState } from "react"
export default function SubscribeForm() {
const [email, setEmail] = useState("")
const handleSubmit = async (e) => {
e.preventDefault()
const response = await fetch("/api/subscribers", {
method: "post",
body: JSON.stringify({ email }),
headers: { "Content-Type": "application/json" }
}).then(res => res.json())
console.log(response)
if (response.created) {
setEmail("")
alert("Email cadastrado com sucesso!")
} else {
alert("Algo deu errado!")
}
}
return (
<form
className="flex justify-center gap-4 p-4"
onSubmit={handleSubmit}
>
<input
type="email"
name="email"
id="email"
placeholder="Seu e-mail principal"
className="bg-slate-800 p-3 rounded"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button className="bg-sky-700 p-3 rounded">
Se inscrever
</button>
</form>
)
}
Por fim, vamos criar uma página que exibe os inscritos e vamos ver como usar os React Server Components para fazer isso no backend. Como já vimos antes, vamos começar criando uma pasta “subscribers” (fora da pasta “api”) e, dentro dela, um “layout.js”:
export default function Layout({ children }) {
return (
<section className="bg-zinc-900 w-full p-10">
<p className="mb-4">Área Administrativa / Inscritos</p>
{children}
</section>
)
}
Agora podemos criar a página que obtém os inscritos no banco de dados e exibe uma tabela:
import mysql from "mysql2/promise"
export const metadata = {
title: "Inscritos | Admin"
}
export default async function Subscribers() {
const db = await mysql.createConnection("mysql://nextjs:nextjs@localhost:3306/nextjs_tutorial")
const [rows] = await db.query("SELECT * FROM Subscribers;")
db.end()
return (
<main>
<h1 className="text-2xl font-bold mb-8">Lista de inscritos</h1>
<table className="w-full text-center">
<thead className="border-b-[1px]">
<tr className="[&>*]:py-4">
<th>ID</th>
<th>Email</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{rows.map((subscriber) => (
<tr key={subscriber.id} className="[&>*]:p-4">
<td>{subscriber.id}</td>
<td className="text-left">{subscriber.email}</td>
<td>{subscriber.createdAt.toDateString()}</td>
</tr>
))}
</tbody>
</table>
</main>
)
}
Para encerrar, vamos ver mais dois componentes especiais do Next.js 13 que são muito úteis, o “loading” e o “error”. Crie um arquivo “loading.js” em alguma rota do diretório “app” para acrescentar automaticamente o tratamento do estado de carregamento de um componente. Crie um arquivo de “loading.js” na pasta “subscribers” para acrescentar esse carregamento enquanto a query não é finalizada no backend:
export default function Loading() {
return (
<main className="grid place-content-center">
<h1 className="text-2xl">
Carregando lista de inscritos...
</h1>
</main>
)
}
Crie também um componente “error.js” nessa mesma pasta. Ele servirá para criar automaticamente um error boundary e lidar com o estado de erro desse componente:
Obs.: repare que o error boundary precisa ser um componente de cliente.
"use client"
export default function Error() {
return (
<main className="grid place-content-center">
<h1 className="text-2xl">
Erro ao carregar inscritos!
</h1>
</main>
)
}
Por fim, podemos testar esses estados de forma artificial no nosso componente de página adicionando uma função de espera e um erro manualmente:
import mysql from "mysql2/promise"
function waitFor(ms) {
return new Promise((resolve) => setTimeout(() => resolve(), ms))
}
export const metadata = {
title: "Inscritos | Admin"
}
export default async function Subscribers() {
const db = await mysql.createConnection("mysql://nextjs:nextjs@localhost:3306/nextjs_tutorial")
const [rows] = await db.query("SELECT * FROM Subscribers;")
db.end()
// await waitFor(5000)
// throw new Error("Deu ruim")
return (
<main>
<h1 className="text-2xl font-bold mb-8">Lista de inscritos</h1>
<table className="w-full text-center">
<thead className="border-b-[1px]">
<tr className="[&>*]:py-4">
<th>ID</th>
<th>Email</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{rows.map((subscriber) => (
<tr key={subscriber.id} className="[&>*]:p-4">
<td>{subscriber.id}</td>
<td className="text-left">{subscriber.email}</td>
<td>{subscriber.createdAt.toDateString()}</td>
</tr>
))}
</tbody>
</table>
</main>
)
}