← → para navegar

Projeto prático: Painel de Produtos

Aula prática guiada do esqueleto visual ao CRUD em memória
Profº Fabrício Malta de Oliveira

Objetivo da prática

  • Entender como uma aplicação React é montada a partir de componentes.
  • Construir primeiro o esqueleto visual da página: sidebar, navbar, conteúdo e footer.
  • Separar o conteúdo em páginas visuais sem começar por regra de negócio.
  • Adicionar dados e fluxo de interação apenas depois que a tela estiver organizada.
  • Trabalhar com TypeScript de forma gradual, tipando props, páginas, produtos e formulários.
2

Resultado final esperado

Produtos
Cadastro
PeriféricosMouse sem fio

R$ 89,90

Ativo
Rodapé do sistema
  • Menu lateral com navegação entre Resumo e Produtos.
  • Área central que troca de conteúdo conforme a página ativa.
  • Lista de produtos renderizada a partir de um array.
  • Busca por nome ou categoria.
  • Formulário para cadastrar e editar produtos.
  • Ações para ativar/inativar e remover itens.
3

Ideia geral da aplicação

A aplicação será pensada como uma árvore de componentes.

main.tsx
monta o React
BrowserRouter
habilita rotas
App.tsx
define as rotas
DashboardLayout
estrutura fixa da rota
Outlet
conteúdo variável
O main.tsx envolve a aplicação com BrowserRouter. O App.tsx define as rotas, o DashboardLayout mantém a estrutura visual fixa e o Outlet mostra a tela correspondente à URL atual.
4

O que fica fixo e o que muda

Fixo na tela

  • Sidebar: menu lateral.
  • Navbar: barra superior.
  • Footer: rodapé.
  • Estrutura geral do dashboard.

Muda na área central

  • Página de resumo.
  • Página de produtos.
  • Lista de produtos.
  • Formulário.
  • Mensagens de estado vazio.
A regra principal é: o layout não deve conhecer os detalhes do CRUD. Ele apenas reserva o espaço onde cada página será exibida.
5

Estrutura de pastas ao final

src/
├── App.tsx
├── main.tsx
├── index.css
├── components/
│   ├── layout/
│   │   ├── DashboardLayout.tsx
│   │   ├── Sidebar.tsx
│   │   ├── Navbar.tsx
│   │   └── Footer.tsx
│   ├── products/
│   │   ├── ProductCard.tsx
│   │   └── ProductForm.tsx
│   └── ui/
│       └── StatCard.tsx
├── data/
│   └── initialProducts.ts
├── pages/
│   ├── DashboardHome.tsx
│   └── ProductsPage.tsx
└── types/
    └── Product.ts
A pasta components guarda partes reutilizáveis. A pasta pages guarda telas completas. A pasta types centraliza tipos TypeScript usados em mais de um arquivo.
6

Antes de codar: ordem de raciocínio

1. Desenhar

montar o layout estático

2. Separar

transformar blocos em componentes

4. Popular

adicionar dados e CRUD

7

Etapa 1: preparar o projeto

Etapa 1

Objetivo

Garantir que o projeto React com TypeScript, Vite e Tailwind está pronto para receber a prática.

Arquivos envolvidos

  • src/App.tsx
  • src/index.css
  • vite.config.ts

Resultado esperado

Ao final, a tela deve abrir no navegador e aceitar classes Tailwind no className.

React + Tailwind

Projeto configurado.

8

Etapa 1 - comandos iniciais

# criar o projeto
npm create vite@latest crud-produtos-react -- --template react-ts

cd crud-produtos-react
npm install

# instalar Tailwind no formato usado com Vite
npm install tailwindcss @tailwindcss/vite

# rodar o projeto
npm run dev
A prática começa apenas depois que o navegador abre a aplicação sem erro.
9

Etapa 1 - configuração mínima do Tailwind

vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

src/index.css

@import "tailwindcss";
Se o Tailwind não estiver funcionando aqui, não avance para o dashboard. Primeiro corrija essa base.
10

Etapa 2: desenhar o layout inteiro no App.tsx

Etapa 2

Objetivo

Criar uma primeira versão visual do dashboard em um único arquivo. Nesta etapa, ainda não haverá componentes separados, dados, páginas ou CRUD.

Arquivos envolvidos

  • src/App.tsx

Resultado esperado

A tela terá sidebar, navbar, área central e footer. O objetivo é enxergar a forma geral da aplicação.
Gestão de produtos
Rodapé do sistema
11

Etapa 2 - App.tsx com esqueleto estático

function App() {
  return (
    <div className="min-h-screen bg-slate-100 text-slate-900">
      <aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 p-6 text-white">
        <h1 className="text-xl font-bold">Admin</h1>

        <nav className="mt-8 space-y-2">
          <button className="w-full rounded-lg bg-slate-700 px-4 py-2 text-left">
            Resumo
          </button>
          <button className="w-full rounded-lg px-4 py-2 text-left hover:bg-slate-800">
            Produtos
          </button>
        </nav>
      </aside>

      <div className="min-h-screen pl-64 flex flex-col">
        <header className="border-b bg-white px-6 py-4">
          <h2 className="text-2xl font-bold">Gestão de produtos</h2>
        </header>

        <main className="flex-1 p-6">
          <section className="rounded-2xl bg-white p-6 shadow-sm">
            <h3 className="text-xl font-bold">Conteúdo central</h3>
            <p className="mt-2 text-slate-600">Aqui entrará a página ativa.</p>
          </section>
        </main>

        <footer className="border-t bg-white px-6 py-4 text-sm text-slate-500">
          Painel de Produtos - React + TypeScript
        </footer>
      </div>
    </div>
  )
}

export default App
12

Etapa 2 - por que começar em um arquivo só?

  • Você vê primeiro vê a tela nascer inteira.
  • A preocupação inicial é visual: onde fica cada parte da página.
  • A separação em componentes vem depois, quando já existe algo concreto para separar.
  • Isso reduz a confusão entre layout, estado, props e regra de negócio.
Nesta etapa, o código ainda não está bonito do ponto de vista de organização. Ele está didático: serve para desenhar a tela.
13

Etapa 3: criar o DashboardLayout

Etapa 3

Objetivo

Transformar a estrutura fixa do dashboard em um componente reutilizável. O conteúdo central será recebido por children.

Arquivos envolvidos

  • src/components/layout/DashboardLayout.tsx
  • src/App.tsx

Resultado esperado

O App deixa de carregar todo o HTML do layout. Ele passa a chamar um componente de layout e enviar o conteúdo central.
Gestão de produtos
Rodapé do sistema
14

Etapa 3 - conceito de children

children é o conteúdo colocado entre a abertura e o fechamento de um componente.

<DashboardLayout>
  <section>Conteúdo central</section>
</DashboardLayout>
O DashboardLayout decide onde esse conteúdo será encaixado. Isso permite reaproveitar o mesmo layout para páginas diferentes.
15

Etapa 3 - DashboardLayout.tsx

import type { ReactNode } from 'react'

type DashboardLayoutProps = {
  children: ReactNode
}

export function DashboardLayout({ children }: DashboardLayoutProps) {
  return (
    <div className="min-h-screen bg-slate-100 text-slate-900">
      <aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 p-6 text-white">
        <h1 className="text-xl font-bold">Admin</h1>

        <nav className="mt-8 space-y-2">
          <button className="w-full rounded-lg bg-slate-700 px-4 py-2 text-left">
            Resumo
          </button>
          <button className="w-full rounded-lg px-4 py-2 text-left hover:bg-slate-800">
            Produtos
          </button>
        </nav>
      </aside>

      <div className="min-h-screen pl-64 flex flex-col">
        <header className="border-b bg-white px-6 py-4">
          <h2 className="text-2xl font-bold">Gestão de produtos</h2>
        </header>

        <main className="flex-1 p-6">{children}</main>

        <footer className="border-t bg-white px-6 py-4 text-sm text-slate-500">
          Painel de Produtos - React + TypeScript
        </footer>
      </div>
    </div>
  )
}
16

Etapa 3 - App.tsx usando o layout

import { DashboardLayout } from './components/layout/DashboardLayout'

function App() {
  return (
    <DashboardLayout>
      <section className="rounded-2xl bg-white p-6 shadow-sm">
        <h3 className="text-xl font-bold">Conteúdo central</h3>
        <p className="mt-2 text-slate-600">Aqui entrará a página ativa.</p>
      </section>
    </DashboardLayout>
  )
}

export default App
Neste momento, a aplicação ainda mostra a mesma tela. A diferença é interna: o layout foi isolado.
17

Etapa 4: separar Sidebar, Navbar e Footer

Etapa 4

Objetivo

Dividir o layout em componentes menores com responsabilidades simples.

Arquivos envolvidos

  • src/components/layout/Sidebar.tsx
  • src/components/layout/Navbar.tsx
  • src/components/layout/Footer.tsx
  • src/components/layout/DashboardLayout.tsx

Resultado esperado

O DashboardLayout passa a montar o layout usando Sidebar, Navbar e Footer.
Gestão de produtos
Rodapé do sistema
18

Etapa 4 - responsabilidades dos componentes

Sidebar

menu lateral e botões de navegação

Navbar

título e informações do topo

Layout

encaixe das partes

Ainda não existe troca de página. Os botões continuam visuais. A navegação entra em uma etapa própria.
19

Etapa 4 - Sidebar.tsx estática

export function Sidebar() {
  return (
    <aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 p-6 text-white">
      <h1 className="text-xl font-bold">Admin</h1>
      <p className="mt-1 text-sm text-slate-400">Painel de produtos</p>

      <nav className="mt-8 space-y-2">
        <button className="w-full rounded-lg bg-slate-700 px-4 py-2 text-left">
          Resumo
        </button>
        <button className="w-full rounded-lg px-4 py-2 text-left hover:bg-slate-800">
          Produtos
        </button>
      </nav>
    </aside>
  )
}
20

Etapa 4 - Navbar.tsx e Footer.tsx

Navbar.tsx

type NavbarProps = {
  title: string
}

export function Navbar({ title }: NavbarProps) {
  return (
    <header className="border-b bg-white px-6 py-4">
      <h2 className="text-2xl font-bold">{title}</h2>
      <p className="text-sm text-slate-500">Gestão de produtos</p>
    </header>
  )
}

Footer.tsx

export function Footer() {
  return (
    <footer className="border-t bg-white px-6 py-4 text-sm text-slate-500">
      Painel de Produtos - React + TypeScript
    </footer>
  )
}
21

Etapa 4 - DashboardLayout.tsx composto

import type { ReactNode } from 'react'
import { Footer } from './Footer'
import { Navbar } from './Navbar'
import { Sidebar } from './Sidebar'

type DashboardLayoutProps = {
  children: ReactNode
}

export function DashboardLayout({ children }: DashboardLayoutProps) {
  return (
    <div className="min-h-screen bg-slate-100 text-slate-900">
      <Sidebar />

      <div className="min-h-screen pl-64 flex flex-col">
        <Navbar title="Gestão de produtos" />
        <main className="flex-1 p-6">{children}</main>
        <Footer />
      </div>
    </div>
  )
}
22

Etapa 5: criar páginas visuais

Etapa 5

Objetivo

Criar componentes que representam telas inteiras: uma página de resumo e uma página de produtos. Ainda não haverá navegação automática.

Arquivos envolvidos

  • src/pages/DashboardHome.tsx
  • src/pages/ProductsPage.tsx
  • src/App.tsx

Resultado esperado

O projeto passa a ter páginas como componentes React. Cada página é apenas uma parte da interface renderizada no centro do layout.
Resumo
Total: 3Ativos: 2Estoque: 49
Bem-vindo ao painel

Escolha uma opção no menu lateral.

Rodapé do sistema
23

Como funcionam pages no React?

React não possui uma pasta especial de páginas por padrão. Uma page é apenas um componente maior que representa uma tela inteira.

Componente pequeno

Botão, card, campo

Componente de layout

Sidebar, Navbar, Footer

A pasta pages é uma convenção de organização. Ela ajuda a separar telas completas de componentes menores.
24

React pages são iguais às rotas do Nest?

NestJS

Uma rota recebe uma requisição HTTP, por exemplo GET /products. O controller responde com dados.

React

Uma página é um componente exibido no navegador. Neste projeto, a troca de tela vai acontecer por rota, usando React Router.

Neste projeto, será usado React Router para trocar entre Resumo e Produtos pela URL. O DashboardLayout será um layout de rota e o conteúdo da tela vai aparecer no Outlet.
25

Etapa 5 - DashboardHome.tsx

export function DashboardHome() {
  return (
    <section className="space-y-6">
      <div>
        <h3 className="text-2xl font-bold">Resumo</h3>
        <p className="text-slate-600">Visão geral do painel administrativo.</p>
      </div>

      <div className="grid gap-4 md:grid-cols-3">
        <article className="rounded-2xl bg-white p-6 shadow-sm">
          <p className="text-sm text-slate-500">Total de produtos</p>
          <strong className="mt-2 block text-3xl">0</strong>
        </article>
        <article className="rounded-2xl bg-white p-6 shadow-sm">
          <p className="text-sm text-slate-500">Produtos ativos</p>
          <strong className="mt-2 block text-3xl">0</strong>
        </article>
        <article className="rounded-2xl bg-white p-6 shadow-sm">
          <p className="text-sm text-slate-500">Estoque total</p>
          <strong className="mt-2 block text-3xl">0</strong>
        </article>
      </div>
    </section>
  )
}
26

Etapa 5 - ProductsPage.tsx temporária

export function ProductsPage() {
  return (
    <section className="rounded-2xl bg-white p-6 shadow-sm">
      <h3 className="text-2xl font-bold">Produtos</h3>
      <p className="mt-2 text-slate-600">
        A lista de produtos será construída nas próximas etapas.
      </p>
    </section>
  )
}
A página nasce simples. Primeiro ela ocupa o lugar certo no layout. Depois ela recebe dados e ações.
27

Etapa 6: trocar páginas com React Router

Etapa 6

Objetivo

Criar a navegação entre telas com React Router. A URL passa a definir qual página aparece, e a Sidebar deixa de controlar estado para apenas apontar para as rotas.

Arquivos envolvidos

  • src/main.tsx
  • src/App.tsx
  • src/components/layout/DashboardLayout.tsx
  • src/components/layout/Sidebar.tsx

Resultado esperado

Ao clicar em Resumo ou Produtos, a URL muda e a área central troca de conteúdo sem recarregar a página.
Produtos
Produtos cadastrados
Rodapé do sistema
28

Etapa 6 - fluxo da navegação por rotas

main.tsx
monta o React
BrowserRouter
habilita as rotas
App.tsx
declara Routes e Route
DashboardLayout
estrutura fixa da rota
Outlet
conteúdo variável
O clique acontece na Sidebar, mas quem decide a página exibida é o React Router. O DashboardLayout continua fixo e o Outlet troca o conteúdo conforme a rota.
O erro mais comum agora é esquecer o BrowserRouter no main.tsx ou esquecer o Outlet dentro do layout. Sem isso, a navegação existe na URL, mas a tela não aparece.
29

Etapa 6 - main.tsx com BrowserRouter

import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
)
O BrowserRouter envolve toda a aplicação e libera os componentes internos para usar Routes, Route, Link e NavLink.
30

Etapa 6 - App.tsx com React Router

import { Route, Routes } from 'react-router-dom'
import { DashboardLayout } from './components/layout/DashboardLayout'
import { DashboardHome } from './pages/DashboardHome'
import { ProductsPage } from './pages/ProductsPage'
function App() {
  return (
    <Routes>
      <Route element={<DashboardLayout />}>
        <Route index element={<DashboardHome />} />
        <Route path="produtos" element={<ProductsPage />} />
      </Route>
    </Routes>
  )
}

export default App
O App só declara as rotas. Quem escolhe a tela ativa é a URL, e o layout recebe o conteúdo correspondente pelo Outlet.
31

Etapa 6 - DashboardLayout.tsx corrigido

import { Outlet } from 'react-router-dom'
import { Footer } from './Footer'
import { Navbar } from './Navbar'
import { Sidebar } from './Sidebar'
export function DashboardLayout() {
  return (
    <div className="min-h-screen bg-slate-100 text-slate-900">
      <Sidebar />

      <div className="min-h-screen pl-64 flex flex-col">
        <Navbar title="Gestão de produtos" />
        <main className="flex-1 p-6"><Outlet /></main>
        <Footer />
      </div>
    </div>
  )
}
O DashboardLayout não recebe mais a página ativa por props. Ele só monta a estrutura fixa e deixa o Outlet ocupar a área do conteúdo.
32

Etapa 6 - Sidebar.tsx com navegação

import { NavLink } from 'react-router-dom'

export function Sidebar() {
  return (
    <aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 p-6 text-white">
      <h1 className="text-xl font-bold">Admin</h1>
      <p className="mt-1 text-sm text-slate-400">Painel de produtos</p>

      <nav className="mt-8 space-y-2">
        <NavLink
          to="/"
          end
          className={({ isActive }) =>
            `block w-full rounded-lg px-4 py-2 text-left transition ${isActive ? 'bg-slate-700 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`
          }
        >
          Resumo
        </NavLink>
        <NavLink
          to="/produtos"
          className={({ isActive }) =>
            `block w-full rounded-lg px-4 py-2 text-left transition ${isActive ? 'bg-slate-700 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`
          }
        >
          Produtos
        </NavLink>
      </nav>
    </aside>
  )
}
O NavLink sabe quando a rota está ativa e aplica o estilo selecionado sem precisar de estado local.
33

Etapa 6 - leitura do fluxo em linguagem simples

  • O usuário clica em Produtos.
  • O NavLink muda a URL para /produtos.
  • O React Router identifica a rota ativa.
  • O DashboardLayout continua o mesmo.
  • O Outlet renderiza ProductsPage no conteúdo central.
  • Não existe troca manual de estado para decidir a tela.
Não houve chamada HTTP, não houve rota de backend e não houve recarregamento da página.
34

Etapa 7: desenhar a página de produtos

Etapa 7

Objetivo

Construir a aparência da página de produtos antes de adicionar o array real.

Arquivos envolvidos

  • src/pages/ProductsPage.tsx

Resultado esperado

A página terá título, campo de busca visual, botão e área onde os cards aparecerão.
Produtos
Produtos cadastrados
CategoriaNome do produto

R$ 0,00

Estoque
CategoriaNome do produto

R$ 0,00

Estoque
Rodapé do sistema
35

Etapa 7 - ProductsPage.tsx visual

export function ProductsPage() {
  return (
    <section className="space-y-6">
      <div className="flex items-center justify-between gap-4">
        <div>
          <h3 className="text-2xl font-bold">Produtos</h3>
          <p className="text-slate-600">Gerencie os produtos cadastrados.</p>
        </div>

        <button className="rounded-lg bg-slate-900 px-4 py-2 text-white hover:bg-slate-700">
          Novo produto
        </button>
      </div>

      <div className="rounded-2xl bg-white p-6 shadow-sm">
        <input
          className="w-full rounded-lg border border-slate-300 px-4 py-2"
          placeholder="Buscar produto..."
        />

        <div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
          <article className="rounded-xl border border-slate-200 p-4">
            <p className="text-sm text-slate-500">Categoria</p>
            <h4 className="text-lg font-bold">Nome do produto</h4>
            <p className="mt-2 font-semibold">R$ 0,00</p>
            <p className="text-sm text-slate-500">Estoque: 0 unidades</p>
          </article>
        </div>
      </div>
    </section>
  )
}
O card ainda é estático. Ele serve como molde visual para o componente ProductCard.
36

Etapa 8: criar tipo e dados iniciais

Etapa 8

Objetivo

Definir o formato de um produto e criar um array inicial para popular a tela.

Arquivos envolvidos

  • src/types/Product.ts
  • src/data/initialProducts.ts

Resultado esperado

Os dados passam a ter uma estrutura previsível: id, nome, categoria, preço, estoque e status.
Produtos
Produtos cadastrados
PeriféricosTeclado mecânico

R$ 249,90

Estoque: 12
MonitoresMonitor 24

R$ 899,90

Estoque: 7
Rodapé do sistema
37

Etapa 8 - Product.ts

export type Product = {
  id: number
  name: string
  category: string
  price: number
  stock: number
  active: boolean
}

export type ProductFormData = {
  name: string
  category: string
  price: number
  stock: number
}
A entidade Product representa o item completo. ProductFormData representa apenas o que o formulário precisa coletar.
38

Etapa 8 - initialProducts.ts

import type { Product } from '../types/Product'

export const initialProducts: Product[] = [
  {
    id: 1,
    name: 'Mouse sem fio',
    category: 'Periféricos',
    price: 89.9,
    stock: 18,
    active: true,
  },
  {
    id: 2,
    name: 'Teclado mecânico',
    category: 'Periféricos',
    price: 249.9,
    stock: 12,
    active: true,
  },
  {
    id: 3,
    name: 'Monitor 24 polegadas',
    category: 'Monitores',
    price: 899.9,
    stock: 7,
    active: false,
  },
]
39

Etapa 9: renderizar produtos com map

Etapa 9

Objetivo

Transformar o array de produtos em elementos visuais na tela.

Arquivos envolvidos

  • src/pages/ProductsPage.tsx
  • src/components/products/ProductCard.tsx

Resultado esperado

Cada produto do array gera um card. O conteúdo deixa de ser fixo.
Produtos
Produtos cadastrados
PeriféricosTeclado mecânico

R$ 249,90

Estoque: 12
MonitoresMonitor 24

R$ 899,90

Estoque: 7
Rodapé do sistema
40

Etapa 9 - ProductCard.tsx

import type { Product } from '../../types/Product'

type ProductCardProps = {
  product: Product
}

export function ProductCard({ product }: ProductCardProps) {
  return (
    <article className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
      <div className="flex items-start justify-between gap-4">
        <div>
          <p className="text-sm text-slate-500">{product.category}</p>
          <h4 className="text-lg font-bold">{product.name}</h4>
        </div>

        <span className={product.active ? 'rounded-full bg-emerald-100 px-3 py-1 text-xs font-bold text-emerald-700' : 'rounded-full bg-slate-200 px-3 py-1 text-xs font-bold text-slate-600'}>
          {product.active ? 'Ativo' : 'Inativo'}
        </span>
      </div>

      <p className="mt-4 text-xl font-bold">
        {product.price.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
      </p>
      <p className="text-sm text-slate-500">Estoque: {product.stock} unidades</p>
    </article>
  )
}
41

Etapa 9 - ProductsPage.tsx com estado de produtos

import { useState } from 'react'
import { ProductCard } from '../components/products/ProductCard'
import { initialProducts } from '../data/initialProducts'
import type { Product } from '../types/Product'

export function ProductsPage() {
  const [products] = useState<Product[]>(initialProducts)

  return (
    <section className="space-y-6">
      <div>
        <h3 className="text-2xl font-bold">Produtos</h3>
        <p className="text-slate-600">Gerencie os produtos cadastrados.</p>
      </div>

      <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </section>
  )
}
O key={product.id} ajuda o React a identificar cada item da lista.
42

Etapa 9 - o que o map faz?

products.map((product) => (
  <ProductCard key={product.id} product={product} />
))
  • Percorre o array products.
  • Para cada item, cria um ProductCard.
  • Envia o produto atual pela prop product.
  • Retorna uma nova lista de elementos React.
43

Etapa 10: adicionar busca

Etapa 10

Objetivo

Criar um estado para o texto digitado e calcular uma lista filtrada a partir da lista original.

Arquivos envolvidos

  • src/pages/ProductsPage.tsx

Resultado esperado

Ao digitar no campo, a tela passa a mostrar apenas os produtos que combinam com a busca.
Produtos
Produtos cadastrados
PeriféricosTeclado mecânico

R$ 249,90

Estoque: 12
MonitoresMonitor 24

R$ 899,90

Estoque: 7
Rodapé do sistema
44

Etapa 10 - ProductsPage.tsx: estado derivado da busca

const [products] = useState<Product[]>(initialProducts)
const [search, setSearch] = useState('')

const normalizedSearch = search.toLowerCase().trim()

const filteredProducts = products.filter((product) => {
  const name = product.name.toLowerCase()
  const category = product.category.toLowerCase()

  return name.includes(normalizedSearch) || category.includes(normalizedSearch)
})
filteredProducts é estado derivado: ele não precisa de useState próprio, porque é calculado a partir de products e search.
Arquivo: src/pages/ProductsPage.tsx. Este bloco fica dentro da função ProductsPage, antes do return. Não coloque isso dentro do JSX. Neste slide, troque a linha const [products] = useState... por este bloco completo. Ainda não mexa no map do return; ele será ajustado no slide 46.
45

Etapa 10 - ProductsPage.tsx: campo de busca e renderização

<input
  value={search}
  onChange={(event) => setSearch(event.target.value)}
  className="w-full rounded-lg border border-slate-300 px-4 py-2"
  placeholder="Buscar por nome ou categoria..."
/>

<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
  {filteredProducts.map((product) => (
    <ProductCard key={product.id} product={product} />
  ))}
</div>
A lista exibida é filteredProducts, não products. O array original continua preservado.
Arquivo: src/pages/ProductsPage.tsx. Este trecho fica dentro do return. Aqui sim você troca a lista antiga por esta: o map continua, mas agora usa filteredProducts.map em vez de products.map.
46

Etapa 11: criar formulário visual

Etapa 11

Objetivo

Montar o formulário de produto sem ainda salvar no array. Primeiro serão criados os campos e a estrutura visual.

Arquivos envolvidos

  • src/components/products/ProductForm.tsx
  • src/pages/ProductsPage.tsx

Resultado esperado

A tela passa a ter uma área de cadastro, mas o botão salvar ainda será conectado na próxima etapa.
Produtos
Cadastro
PeriféricosMouse sem fio

R$ 89,90

Ativo
Rodapé do sistema
47

Etapa 11 - ProductForm.tsx com estado interno

import { useState } from 'react'
import type { ProductFormData } from '../../types/Product'

type ProductFormProps = {
  onSubmit: (data: ProductFormData) => void
}

const initialForm: ProductFormData = {
  name: '',
  category: '',
  price: 0,
  stock: 0,
}

export function ProductForm({ onSubmit }: ProductFormProps) {
  const [form, setForm] = useState<ProductFormData>(initialForm)

  function updateField(field: keyof ProductFormData, value: string) {
    setForm((current) => ({
      ...current,
      [field]: field === 'price' || field === 'stock' ? Number(value) : value,
    }))
  }

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    onSubmit(form)
    setForm(initialForm)
  }

  return (
    <form onSubmit={handleSubmit} className="rounded-2xl bg-white p-6 shadow-sm">
      <h4 className="text-lg font-bold">Cadastrar produto</h4>
      {/* campos entram no próximo slide */}
    </form>
  )
}
48

Etapa 11 - campos do formulário

<div className="mt-4 grid gap-4 md:grid-cols-2">
  <input
    value={form.name}
    onChange={(event) => updateField('name', event.target.value)}
    className="rounded-lg border border-slate-300 px-4 py-2"
    placeholder="Nome"
  />

  <input
    value={form.category}
    onChange={(event) => updateField('category', event.target.value)}
    className="rounded-lg border border-slate-300 px-4 py-2"
    placeholder="Categoria"
  />

  <input
    type="number"
    value={form.price}
    onChange={(event) => updateField('price', event.target.value)}
    className="rounded-lg border border-slate-300 px-4 py-2"
    placeholder="Preço"
  />

  <input
    type="number"
    value={form.stock}
    onChange={(event) => updateField('stock', event.target.value)}
    className="rounded-lg border border-slate-300 px-4 py-2"
    placeholder="Estoque"
  />
</div>

<button className="mt-4 rounded-lg bg-slate-900 px-4 py-2 text-white">
  Salvar produto
</button>
49

Etapa 12: cadastrar produto

Etapa 12

Objetivo

Conectar o formulário ao estado products, criando um novo produto no array.

Arquivos envolvidos

  • src/pages/ProductsPage.tsx
  • src/components/products/ProductForm.tsx

Resultado esperado

Ao preencher o formulário e salvar, um novo card aparece na lista.
Produtos
Cadastro
PeriféricosMouse sem fio

R$ 89,90

Ativo
Rodapé do sistema
50

Etapa 12 - função handleCreate em ProductsPage

const [products, setProducts] = useState<Product[]>(initialProducts)

function handleCreate(data: ProductFormData) {
  const newProduct: Product = {
    id: Date.now(),
    name: data.name,
    category: data.category,
    price: data.price,
    stock: data.stock,
    active: true,
  }

  setProducts((currentProducts) => [newProduct, ...currentProducts])
}
Date.now() é suficiente para a prática em memória. Em um backend real, normalmente o id viria do banco de dados.
Arquivo: src/pages/ProductsPage.tsx. Substitua o estado antigo const [products] = useState... por const [products, setProducts] = useState.... Não crie um segundo useState para products. O setProducts é necessário para cadastrar, editar, inativar e excluir produtos nas próximas etapas.
51

Etapa 12 - usando o ProductForm na página

import { ProductForm } from '../components/products/ProductForm'

// dentro do return de ProductsPage
<ProductForm onSubmit={handleCreate} />

<input
  value={search}
  onChange={(event) => setSearch(event.target.value)}
  className="w-full rounded-lg border border-slate-300 px-4 py-2"
  placeholder="Buscar por nome ou categoria..."
/>

<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
  {filteredProducts.map((product) => (
    <ProductCard key={product.id} product={product} />
  ))}
</div>
O ProductForm não altera o array diretamente. Ele entrega os dados para ProductsPage por meio da função onSubmit.
52

Etapa 13: inativar e excluir produtos

Etapa 13

Objetivo

Adicionar ações simples em cada card. Essas ações alteram o array usando map e filter.

Arquivos envolvidos

  • src/pages/ProductsPage.tsx
  • src/components/products/ProductCard.tsx

Resultado esperado

Cada produto terá botões para ativar/inativar e excluir.
Produtos
Cadastro
PeriféricosMouse sem fio

R$ 89,90

Ativo
Rodapé do sistema
53

Etapa 13 - funções em ProductsPage

function handleToggleActive(id: number) {
  setProducts((currentProducts) =>
    currentProducts.map((product) =>
      product.id === id
        ? { ...product, active: !product.active }
        : product,
    ),
  )
}

function handleDelete(id: number) {
  setProducts((currentProducts) =>
    currentProducts.filter((product) => product.id !== id),
  )
}
map altera um item mantendo os demais. filter remove um item criando uma nova lista.
54

Etapa 13 - ProductCard recebendo ações

type ProductCardProps = {
  product: Product
  onToggleActive: (id: number) => void
  onDelete: (id: number) => void
}

export function ProductCard({ product, onToggleActive, onDelete }: ProductCardProps) {
  return (
    <article className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
      <div className="flex items-start justify-between gap-4">
        <div>
          <p className="text-sm text-slate-500">{product.category}</p>
          <h4 className="text-lg font-bold">{product.name}</h4>
        </div>

        <span className={product.active ? 'rounded-full bg-emerald-100 px-3 py-1 text-xs font-bold text-emerald-700' : 'rounded-full bg-slate-200 px-3 py-1 text-xs font-bold text-slate-600'}>
          {product.active ? 'Ativo' : 'Inativo'}
        </span>
      </div>

      <p className="mt-4 text-xl font-bold">
        {product.price.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
      </p>
      <p className="text-sm text-slate-500">Estoque: {product.stock} unidades</p>

      <div className="mt-4 flex gap-2">
        <button
          type="button"
          onClick={() => onToggleActive(product.id)}
          className="rounded-lg border px-3 py-2 text-sm"
        >
          {product.active ? 'Inativar' : 'Ativar'}
        </button>

        <button
          type="button"
          onClick={() => onDelete(product.id)}
          className="rounded-lg border px-3 py-2 text-sm text-red-700"
        >
          Excluir
        </button>
      </div>
    </article>
  )
}
Arquivo: src/components/products/ProductCard.tsx. Mantenha as informações do produto no card: categoria, nome, status, preço e estoque. Nesta etapa você só acrescenta as props de ação e os botões; não apague o bloco visual criado na Etapa 9.
55

Etapa 13 - passando ações para o card

{filteredProducts.map((product) => (
  <ProductCard
    key={product.id}
    product={product}
    onToggleActive={handleToggleActive}
    onDelete={handleDelete}
  />
))}
O card não sabe como alterar o array. Ele apenas chama a função que recebeu da página.
Arquivo: src/pages/ProductsPage.tsx. Este trecho fica dentro do return, no mesmo lugar onde já existe o filteredProducts.map. Não crie outro map: substitua o <ProductCard ... /> antigo por este, adicionando as props onToggleActive e onDelete.
56

Etapa 14: editar produto

Etapa 14

Objetivo

Criar um modo de edição reaproveitando o mesmo formulário usado para cadastrar.

Arquivos envolvidos

  • src/pages/ProductsPage.tsx
  • src/components/products/ProductForm.tsx
  • src/components/products/ProductCard.tsx

Resultado esperado

Ao clicar em editar, o formulário é preenchido com os dados do produto selecionado. Ao salvar, o item é atualizado no array.
Produtos
Cadastro
PeriféricosMouse sem fio

R$ 89,90

Ativo
Rodapé do sistema
57

Etapa 14 - estado de edição em ProductsPage

const [editingProduct, setEditingProduct] = useState<Product | null>(null)

function handleStartEdit(product: Product) {
  setEditingProduct(product)
}

function handleCancelEdit() {
  setEditingProduct(null)
}

function handleSave(data: ProductFormData) {
  if (!editingProduct) {
    handleCreate(data)
    return
  }

  setProducts((currentProducts) =>
    currentProducts.map((product) =>
      product.id === editingProduct.id
        ? { ...product, ...data }
        : product,
    ),
  )

  setEditingProduct(null)
}
58

Etapa 14 - ProductForm com initialData

type ProductFormProps = {
  onSubmit: (data: ProductFormData) => void
  initialData?: Product | null
  onCancel?: () => void
}

export function ProductForm({ onSubmit, initialData, onCancel }: ProductFormProps) {
  const [form, setForm] = useState<ProductFormData>(initialForm)

  useEffect(() => {
    if (initialData) {
      setForm({
        name: initialData.name,
        category: initialData.category,
        price: initialData.price,
        stock: initialData.stock,
      })
    } else {
      setForm(initialForm)
    }
  }, [initialData])

  // restante do formulário permanece igual
}
Para esse código funcionar, importe useEffect junto com useState: import { useEffect, useState } from 'react'.
59

Etapa 14 - usando o formulário para criar ou editar

<ProductForm
  onSubmit={handleSave}
  initialData={editingProduct}
  onCancel={handleCancelEdit}
/>

{filteredProducts.map((product) => (
  <ProductCard
    key={product.id}
    product={product}
    onEdit={handleStartEdit}
    onToggleActive={handleToggleActive}
    onDelete={handleDelete}
  />
))}
Se editingProduct for null, o formulário cria. Se editingProduct tiver um produto, o formulário edita.
Arquivo: src/pages/ProductsPage.tsx. Este trecho fica dentro do return. Substitua o <ProductForm onSubmit={handleCreate} /> antigo por este ProductForm com handleSave, initialData e onCancel. No mesmo filteredProducts.map, mantenha o map e adicione onEdit={handleStartEdit} ao ProductCard.
60

Etapa 14 - botão Editar no ProductCard

type ProductCardProps = {
  product: Product
  onEdit: (product: Product) => void
  onToggleActive: (id: number) => void
  onDelete: (id: number) => void
}

export function ProductCard({
  product,
  onEdit,
  onToggleActive,
  onDelete,
}: ProductCardProps) {
  // restante do card permanece igual
}

<button
  type="button"
  onClick={() => onEdit(product)}
  className="rounded-lg border px-3 py-2 text-sm"
>
  Editar
</button>
Arquivo: src/components/products/ProductCard.tsx. Além de adicionar onEdit no tipo ProductCardProps, coloque também onEdit nos parâmetros recebidos pela função ProductCard. Sem isso, o botão Editar não consegue chamar a função.
61

Fluxo completo do CRUD em memória

ProductForm
coleta dados
ProductsPage
recebe via onSubmit
setProducts
cria nova lista
ProductCard
renderiza resultado
O CRUD é em memória. Ao recarregar a página, os dados voltam para initialProducts. Persistência em backend ou localStorage pode ser uma continuação da aula.
62

Ponto crítico: erro com rotas

  • Se App.tsx usa <Route element={<DashboardLayout />}>, o layout precisa renderizar o Outlet para mostrar as páginas filhas.
  • Se a Sidebar usa NavLink, o caminho precisa bater com a rota declarada em App.tsx.
  • O BrowserRouter precisa envolver a aplicação inteira no main.tsx.
  • O caminho precisa bater exatamente: / e /produtos.
  • O DashboardLayout não deve decidir a página por conta própria; ele só exibe a estrutura comum.
Sintoma comum: a URL muda, mas a tela não troca. Correção: conferir BrowserRouter, Route, NavLink e Outlet.
63

Checklist de teste após cada etapa

  • O projeto compila sem erro no terminal.
  • A página abre no navegador.
  • O visual esperado aparece antes de seguir.
  • Os botões de navegação trocam Resumo e Produtos.
  • A lista aparece a partir do array.
  • A busca filtra sem apagar dados.
  • Cadastrar, editar, inativar e excluir funcionam sem recarregar a página.
Se uma etapa quebrar, volte para o último ponto que funcionava. Não acumule erro de layout, tipo e estado ao mesmo tempo.
64

Roteiro de commits sugerido

git add .
git commit -m "chore: configure project"

git add .
git commit -m "feat: create static dashboard layout"

git add .
git commit -m "refactor: split dashboard layout components"

git add .
git commit -m "feat: add internal page navigation"

git add .
git commit -m "feat: render products from state"

git add .
git commit -m "feat: add product search and form"

git add .
git commit -m "feat: add product edit and delete actions"
65

Ordem final da prática

Momento Foco Resultado
1 Projeto e Tailwind Base funcionando
2 Layout estático Dashboard desenhado
3 Componentização Layout separado
4 Pages Resumo e Produtos como telas
5 Navegação por rotas React Router funcionando
6 Dados Produtos renderizados
7 Interação Busca e formulário
8 CRUD Criar, editar, inativar e excluir
66

Atividade

  • Adicionar uma página chamada Relatórios usando uma nova rota.
  • Criar um StatCard reutilizável para os cards do resumo.
  • Mostrar no resumo o total de produtos, produtos ativos e estoque total.
  • Adicionar uma mensagem quando a busca não encontrar nenhum produto.
  • Impedir cadastro de produto com nome vazio ou preço menor que zero.
Essas tarefas testam layout, props, estado, listas, formulário e atualização imutável.
67

FIM

Projeto prático: React + TypeScript + Vite
Painel de produtos construído por etapas
68