📚 Módulo 08: Frontend - Carrinho Reativo (Hooks) e CRUD Admin

Neste módulo, programaremos a inteligência interativa do frontend da TecLoja 03. Criaremos um Custom Hook chamado useCart para gerenciar de forma totalmente reativa o estado do carrinho de compras e recalcular totais de forma instantânea. Desenvolveremos a tela de Faturamento de Pedido (Checkout) enviando requisições à API NestJS, e criaremos a interface de administração para o CRUD de Produtos completa com validação visual de formulários.


🔄 Reatividade de Estado (Hook)

A grande vantagem de centralizar a inteligência em um Hook customizado é que múltiplos componentes podem ler os dados e reagir simultaneamente sem duplicação de código.

stateDiagram-v2
    [*] --> HookuseCart
    HookuseCart --> LocalStorage : Salva (Persistência)
    HookuseCart --> TelaCarrinho : Retorna Arrays e Funções
    HookuseCart --> Navbar : Retorna totalItens (Notificação)

    state TelaCarrinho {
        adicionar()
        remover()
    }

🛒 1. O Gancho do Carrinho Reativo (useCart)

No React, os Custom Hooks permitem extrair a lógica de estado de um componente para que ela seja reaproveitada de forma modular em qualquer parte da aplicação.

Criaremos a inteligência que controla a inserção, remoção, alteração de quantidades de produtos no carrinho e recálculo dinâmico de totais. Crie o arquivo em src/hooks/useCart.ts:

// src/hooks/useCart.ts
import { useState, useEffect } from 'react';
import { Produto, ItemPedidoInput } from '../types';

interface ItemCarrinho {
  produto: Produto;
  quantidade: number;
}

export const useCart = () => {
  const [itens, setItens] = useState<ItemCarrinho[]>([]);

  // 1. Carregar carrinho persistido no localStorage na inicialização
  useEffect(() => {
    const carrinhoSalvo = localStorage.getItem('tecloja_carrinho');
    if (carrinhoSalvo) {
      setItens(JSON.parse(carrinhoSalvo));
    }
  }, []);

  // 2. Gravar no localStorage sempre que o carrinho for modificado
  const persistirCarrinho = (novosItens: ItemCarrinho[]) => {
    localStorage.setItem('tecloja_carrinho', JSON.stringify(novosItens));
    setItens(novosItens);
  };

  const adicionarItem = (produto: Produto, quantidade = 1) => {
    const novosItens = [...itens];
    const index = novosItens.findIndex((item) => item.produto.id === produto.id);

    if (index > -1) {
      // Se o produto já está no carrinho, incrementa a quantidade
      novosItens[index].quantidade += quantidade;
    } else {
      // Se não, adiciona um novo item
      novosItens.push({ produto, quantidade });
    }

    persistirCarrinho(novosItens);
  };

  const removerItem = (produtoId: number) => {
    const novosItens = itens.filter((item) => item.produto.id !== produtoId);
    persistirCarrinho(novosItens);
  };

  const alterarQuantidade = (produtoId: number, quantidade: number) => {
    if (quantidade < 1) return;
    
    const novosItens = itens.map((item) => {
      if (item.produto.id === produtoId) {
        return { ...item, quantidade };
      }
      return item;
    });

    persistirCarrinho(novosItens);
  };

  const limparCarrinho = () => {
    persistirCarrinho([]);
  };

  // Recálculo dinâmico de totais (Equivalente reativo a Signals/Computed)
  const totalItens = itens.reduce((acumulado, item) => acumulado + item.quantidade, 0);
  const valorTotal = itens.reduce((acumulado, item) => acumulado + Number(item.produto.preco) * item.quantidade, 0);

  return {
    itens,
    totalItens,
    valorTotal,
    adicionarItem,
    removerItem,
    alterarQuantidade,
    limparCarrinho,
  };
};

💳 2. Integrando a Tela de Checkout (Faturamento)

Abaixo, criaremos o componente de visualização da tela do Carrinho de Compras em src/views/Carrinho.tsx, integrando o gancho useCart e enviando os dados de compra atômica para a nossa API transacional do NestJS:

// src/views/Carrinho.tsx
import React, { useState } from 'react';
import { useCart } from '../hooks/useCart';
import api from '../services/api';
import { CheckoutInput } from '../types';

const Carrinho: React.FC = () => {
  const { itens, valorTotal, removerItem, alterarQuantidade, limparCarrinho } = useCart();
  const [comprado, setComprado] = useState(false);
  const [carregando, setCarregando] = useState(false);
  const [erro, setErro] = useState<string | null>(null);

  const handleFinalizarCompra = async () => {
    setCarregando(true);
    setErro(null);

    const payload: CheckoutInput = {
      clienteId: 1, // Simulação de Cliente de teste semeado no banco no Módulo 02
      itens: itens.map((item) => ({
        produtoId: item.produto.id,
        quantidade: item.quantidade,
      })),
    };

    try {
      // POST para a API NestJS
      await api.post('/pedidos/checkout', payload);
      limparCarrinho();
      setComprado(true);
    } catch (err: any) {
      console.error(err);
      // Exibe a mensagem limpa formatada pelo HttpExceptionFilter do NestJS
      setErro(err.response?.data?.message || 'Erro inesperado ao processar o faturamento.');
    } finally {
      setCarregando(false);
    }
  };

  if (comprado) {
    return (
      <div className="card-glass text-center">
        <h2>🎉 Pedido Aprovado!</h2>
        <p>Sua compra foi processada com sucesso no banco de dados.</p>
        <button className="btn-premium btn-primario" onClick={() => setComprado(false)}>Voltar ao Catálogo</button>
      </div>
    );
  }

  return (
    <div className="grid-carrinho">
      <div className="card-glass">
        <h2>🛒 Seu Carrinho</h2>
        {itens.length === 0 ? (
          <p>O seu carrinho está vazio no momento.</p>
        ) : (
          itens.map((item) => (
            <div key={item.produto.id} className="item-carrinho">
              <span>{item.produto.nome}</span>
              <input
                type="number"
                min="1"
                value={item.quantidade}
                onChange={(e) => alterarQuantidade(item.produto.id, Number(e.target.value))}
              />
              <span>R$ {(Number(item.produto.preco) * item.quantidade).toFixed(2)}</span>
              <button onClick={() => removerItem(item.produto.id)}>Remover</button>
            </div>
          ))
        )}
      </div>

      <div className="card-glass">
        <h3>Resumo do Pedido</h3>
        <p>Total Geral: R$ {valorTotal.toFixed(2)}</p>
        {erro && <div className="erro-alerta">{erro}</div>}
        <button
          className="btn-premium btn-primario"
          disabled={itens.length === 0 || carregando}
          onClick={handleFinalizarCompra}
        >
          {carregando ? 'Processando transação...' : 'Confirmar Compra'}
        </button>
      </div>
    </div>
  );
};

export default Carrinho;

💻 3. Formulário de CRUD com Validações Manuais Reativas

Para gerenciar o cadastro e atualização de eletrônicos no e-commerce, criaremos o formulário administrativo em src/views/admin/ProdutoForm.tsx. O componente usará validações manuais acionadas por propriedades em tempo de preenchimento, fornecendo um feedback visual impecável ao usuário antes de salvar dados via requisição HTTP:

```tsx // src/views/admin/ProdutoForm.tsx import React, { useState, useEffect } from ‘react’; import { useNavigate, useParams } from ‘react-router-dom’; import api from ‘../../services/api’; import { Categoria } from ‘../../types’;

const ProdutoForm: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const editando = !!id;

const [nome, setNome] = useState(‘’); const [descricao, setDescricao] = useState(‘’); const [preco, setPreco] = useState(0); const [estoque, setEstoque] = useState(0); const [categoriaId, setCategoriaId] = useState(0); const [categorias, setCategorias] = useState<Categoria[]>([]);

// Estados de Validação do Formulário (Reação nativa sem dependências) const [erros, setErros] = useState<{ [campo: string]: string }>({}); const [carregando, setCarregando] = useState(false);

useEffect(() => { // Carregar categorias existentes para preencher a seleção api.get<Categoria[]>(‘/categorias’).then((res) => setCategorias(res.data));

if (editando) {
  api.get(`/produtos/${id}`).then((res) => {
    setNome(res.data.nome);
    setDescricao(res.data.descricao || '');
    setPreco(Number(res.data.preco));
    setEstoque(Number(res.data.estoque));
    setCategoriaId(res.data.categoriaId);
  });
}   }, [id, editando]);

// Função validadora manual reativa const validarFormulario = (): boolean => { const errosDetectados: { [campo: string]: string } = {};

if (!nome.trim()) errosDetectados.nome = 'O nome do produto é obrigatório.';
if (preco <= 0) errosDetectados.preco = 'O preço deve ser um valor positivo maior que zero.';
if (estoque < 0) errosDetectados.estoque = 'A quantidade de estoque não pode ser negativa.';
if (categoriaId <= 0) errosDetectados.categoriaId = 'Selecione uma categoria de produto válida.';

setErros(errosDetectados);
return Object.keys(errosDetectados).length === 0;   };

const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validarFormulario()) return; // Aborta envio se houver falhas

setCarregando(true);
const payload = { nome, descricao, preco, estoque, categoriaId };

try {
  if (editando) {
    await api.put(`/produtos/${id}`, payload);
  } else {
    await api.post('/produtos', payload);
  }
  navigate('/admin/produtos');
} catch (err: any) {
  console.error(err);
  alert(err.response?.data?.message || 'Erro ao registrar informações do produto.');
} finally {
  setCarregando(false);
}   };

return ( <div className="card-glass max-w-lg"> <h2>{editando ? ‘📝 Editar Eletrônico’ : ‘✨ Cadastrar Novo Produto’}</h2> <form onSubmit={handleSubmit} className=”form-estrutura”>

    <div className="form-grupo">
      <label>Nome do Produto</label>
      <input
        type="text"
        className="form-campo"
        value={nome}
        onChange={(e) => setNome(e.target.value)}
      />
      {erros.nome && <span className="erro-texto">{erros.nome}</span>}
    </div>

    <div className="form-grupo">
      <label>Preço de Venda (R$)</label>
      <input
        type="number"
        step="0.01"
        className="form-campo"
        value={preco}
        onChange={(e) => setPreco(Number(e.target.value))}
      />
      {erros.preco && <span className="erro-texto">{erros.preco}</span>}
    </div>

    <div className="form-grupo">
      <label>Estoque Inicial</label>
      <input
        type="number"
        className="form-campo"
        value={estoque}
        onChange={(e) => setEstoque(Number(e.target.value))}
      />
      {erros.estoque && <span className="erro-texto">{erros.estoque}</span>}
    </div>

    <div className="form-grupo">
      <label>Categoria de Catálogo</label>
      <select
        className="form-campo"
        value={categoriaId}
        onChange={(e) => setCategoriaId(Number(e.target.value))}
      >
        <option value="0">Selecione uma Categoria...</option>
        {categorias.map((cat) => (
          <option key={cat.id} value={cat.id}>{cat.nome}</option>
        ))}
      </select>
      {erros.categoriaId && <span className="erro-texto">{erros.categoriaId}</span>}
    </div>

    <button type="submit" className="btn-premium btn-primario" disabled={carregando}>
      {carregando ? 'Salvando dados...' : 'Confirmar e Salvar'}
    </button>
  </form>
</div>   ); };

export default ProdutoForm;


✅ Pré-Requisitos deste Módulo

Antes de passar para a fase final de testes, conteinerização Docker e pipelines de CI/CD, certifique-se de que:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Reatividade nas Views: Adicione um produto ao carrinho na vitrine e navegue até a rota /carrinho. Confirme se o item está listado e se a alteração de quantidade atualiza o preço total em tempo real de forma automática.
  2. Validação de Preenchimento: Tente submeter o formulário de cadastro de produtos deixando o preço zerado ou a categoria em branco. Garanta que a aplicação impeça o envio e exiba alertas visuais abaixo dos respectivos campos.
  3. Transação Concluída: Finalize uma compra no carrinho e verifique se os estoques dos respectivos produtos no banco de dados (Neon) foram debitados atômicamente por meio das requisições privadas da API.

⚠️ Erros Comuns

Erro Causa Solução
Os itens do carrinho somem ou resetam após recarregar a tela (F5) O useEffect responsável por recuperar os dados do local storage na inicialização do hook falhou ou a chave de armazenamento está incorreta. Garanta que o nome da chave usada no localStorage.getItem e setItem é exatamente igual (tecloja_carrinho) e realize o parse do JSON de forma protegida.
Erro 400 Bad Request no checkout alegando que o clienteId não existe O ID de cliente enviado no payload da requisição de checkout não corresponde a nenhum registro físico no banco de dados Neon. Certifique-se de preencher a variável clienteId com o identificador de um cliente existente semeado pelo script de seed (ex: clienteId: 1).
O seletor de categorias do formulário administrativo inicia vazio ou não preenche os dados A rota pública /categorias do NestJS está inacessível ou ocorreu erro de conexões antes do componente React carregar. Verifique se a API do backend está ativa, garanta a correta importação e tratamento do array de categorias no useEffect de montagem e certifique-se de que o CORS está habilitado no backend.

🏁 Conclusão

Com o carrinho reativo em funcionamento e as telas de administração e formulários de cadastros e modificações de eletrônicos finalizadas de forma brilhante, a experiência de interface do e-commerce da TecLoja 03 está totalmente pronta para interação!

No Módulo 09, entraremos na fase final do nosso curso: DevOps e Qualidade de Código. Programaremos a suíte de testes de backend utilizando o Jest, conteinerizaremos a API NestJS e o frontend React (usando Nginx de forma otimizada para rotas virtuais de SPA) com Docker Multi-Stage e configuraremos os pipelines automáticos de CI/CD do GitHub Actions.


Voltar para o Sumário