Competições Senac RS · Seletiva 26
Módulo 05 · Apostila teórica
Módulo 05 — Front-end web
Por que esse é o módulo mais gordo. O front vale 40% da nota, mais que back e banco somados. É o Módulo C da prova (4 horas, o mais longo). É a parte que a banca vê funcionando, e "o que a banca vê" é metade do jogo (lembra do módulo 01). Aqui é onde 2 vira 3 ou vira 1. Natureza 🔄 revisão, as duas já fizeram front na Regional, mas é aqui que a gente coloca mais energia de treino, porque é onde tem mais nota pra ganhar e mais pra perder. O combinado do módulo: uma stack só no front (HTML + CSS + JavaScript puro, sem framework), porque é o que a prova permite com segurança e o que dá controle total sob pressão. Todo código desse módulo conversa com a API que vocês construíram no módulo 04: mesmas rotas, mesmo token, mesmo formato de erro.
1. As três camadas e a organização do projeto
Todo front é feito de três linguagens com papéis distintos. Misturar os papéis é a origem de metade da bagunça:
- HTML = a estrutura. O esqueleto: quais elementos existem na página (títulos, formulários, tabelas, botões).
- CSS = a aparência. Cor, espaçamento, tamanho, posição, tipografia. Como as coisas se parecem.
- JavaScript = o comportamento. O que acontece quando o usuário clica, digita, envia. E a conversa com a API.
🛠️ Gambiarra boa: separação de responsabilidades. Estrutura no HTML, estilo no CSS, lógica no JS. Não jogar estilo dentro do HTML (style= no meio da tag) nem lógica embolada com aparência. Código separado é código que a banca lê fácil (ponto de julgamento) e que você conserta rápido sob pressão.
1.1 A pasta do front
A separação de papéis vira separação de arquivos. O projeto do Módulo C nasce assim nos primeiros minutos:
front/
├── index.html ← login (a porta de entrada)
├── convidados.html ← lista + cadastro
├── checkin.html ← a tela da recepção
├── dashboard.html ← indicadores da gestão
├── css/
│ └── style.css ← um CSS só, compartilhado por todas as páginas
└── js/
├── api.js ← comunicação com a API (seção 5.6)
├── auth.js ← token, guarda de página, perfil (seção 8)
├── convidados.js ← lógica da tela de convidados
├── checkin.js
└── dashboard.js
Decisões embutidas aí:
- Uma página HTML por tela (multi-página). É o caminho mais simples e mais seguro pra prova: cada tela é um arquivo, F5 sempre funciona, um erro numa tela não derruba as outras. A alternativa SPA fica na seção 9.
- Um CSS só. Todas as telas compartilham
style.css. Isso força consistência visual (mesmos botões, mesmos cards em toda tela) e consistência pontua. - JS por tela + dois arquivos de base (
api.jseauth.js) que toda página importa. Escreve uma vez, usa em tudo.
Os scripts entram no fim com defer, que garante que o HTML já existe quando o JS rodar:
<script src="js/api.js" defer></script>
<script src="js/auth.js" defer></script>
<script src="js/convidados.js" defer></script>
⚠️ Pega-ratão: script no <head> sem defer roda antes do HTML existir. Aí o querySelector devolve null e o console grita Cannot read properties of null. É um dos erros mais comuns de front e se resolve com uma palavra.
2. HTML semântico: estrutura que significa algo
HTML semântico é usar a tag certa pro sentido certo, em vez de div pra tudo. Um cabeçalho é header, uma navegação é nav, o conteúdo principal é main, um botão é button.
Por que importa:
- Acessibilidade: leitores de tela entendem a página. Isso conecta com UX (módulo 06) e é exigência explícita da UC15.
- Legibilidade: o código conta sozinho o que é cada parte. A banca lê o fonte.
- Comportamento de graça:
buttonresponde a Enter e a Tab sem você escrever nada.formdispara submit. A tag certa já vem com o comportamento embutido.
2.1 O esqueleto de toda página
Esse bloco abre todo HTML da prova. Vale digitar de memória:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wedding Pass · Convidados</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header class="navbar"> ... </header>
<main> ... </main>
<script src="js/api.js" defer></script>
</body>
</html>
lang="pt-BR"diz o idioma (acessibilidade e corretor do navegador).charset="UTF-8"evita acento quebrado (ã virando símbolo estranho).meta viewporté a linha que liga a responsividade. Sem ela, o celular finge ser um desktop de 980px e encolhe tudo: suas media queries da seção 4 simplesmente não disparam.
⚠️ Pega-ratão clássico da prova: tela "responsiva" que funciona no DevTools do notebook e quebra no celular de verdade. Causa número 1: esqueceu a meta viewport. É a segunda linha do <head>, sempre.
2.2 As tags que a prova usa
Não precisa decorar a especificação inteira. Essa tabela cobre o Wedding Pass:
| Tag | Pra quê | Onde aparece na prova |
|---|---|---|
header |
topo da página ou de uma seção | barra superior com logo e navegação |
nav |
bloco de navegação | menu entre as telas |
main |
conteúdo principal (um por página) | tudo que não é topo nem rodapé |
section |
agrupamento temático com título | bloco de indicadores, bloco de busca |
article |
item independente e completo | um card de convidado |
footer |
rodapé | créditos, versão |
h1...h3 |
hierarquia de títulos (sem pular níveis) | h1 o título da tela, h2 as seções |
form, label, input, select, button |
entrada de dados | cadastro, login, busca |
table, thead, tbody, th, td |
dados tabulares | lista de convidados em tela larga |
ul, li |
listas | menu, lista de erros de validação |
strong, span |
ênfase e gancho de estilo | destaque de números, badges |
⚠️ Pega-ratão: a "div soup". Página inteira feita de div genérica, botão que é um div com clique. Funciona visualmente, mas é frágil, inacessível e nível baixo de qualidade. Um div com onclick não recebe foco por Tab, não dispara com Enter e é invisível pra leitor de tela. button faz tudo isso de graça. div e span existem, mas são o último recurso (agrupar pra estilizar), não o primeiro.
2.3 O formulário bem feito
Formulário é onde marcação semântica vira nota mais rápido. O cadastro de convidado, do jeito certo:
<form id="form-convidado" novalidate>
<div class="campo">
<label for="nome">Nome completo</label>
<input type="text" id="nome" name="nome" required minlength="3"
placeholder="Maria da Silva">
<p class="campo-erro" hidden></p>
</div>
<div class="campo">
<label for="email">E-mail</label>
<input type="email" id="email" name="email" placeholder="maria@email.com">
<p class="campo-erro" hidden></p>
</div>
<div class="campo">
<label for="telefone">Telefone</label>
<input type="tel" id="telefone" name="telefone" placeholder="(51) 99999-9999">
<p class="campo-erro" hidden></p>
</div>
<div class="campo">
<label for="acompanhantes">Acompanhantes (máx. 5)</label>
<input type="number" id="acompanhantes" name="acompanhantes" min="0" max="5" value="0">
<p class="campo-erro" hidden></p>
</div>
<div class="acoes">
<button type="button" class="botao-secundario" id="btn-cancelar">Cancelar</button>
<button type="submit" class="botao-primario">Salvar convidado</button>
</div>
</form>
O que esse bloco carrega:
labelligado porfor/id: clicar no rótulo foca o campo, e o leitor de tela anuncia o nome do campo. É o item de acessibilidade mais barato que existe e a banca enxerga no fonte em segundos.typecerto em cada input:emailvalida formato sozinho,numbercommin/maxlimita acompanhantes,telabre teclado numérico no celular,passwordesconde a senha no login. Validação e usabilidade de graça.nameem cada campo: é o que permite ler o form inteiro de uma vez comFormData(seção 5.1).- Um lugar reservado pra erro por campo (
.campo-erro): a validação da seção 10 vai usar. novalidateno form: desliga os balões nativos do navegador porque a gente vai mostrar mensagens melhores via JS (seção 10). Sem a validação própria pronta, não usarnovalidate.type="button"no botão que não envia: botão dentro de form ésubmitpor padrão. Um "Cancelar" semtype="button"envia o formulário. Pega-ratão silencioso.
3. CSS de verdade: do seletor ao layout
CSS é onde os 40% mais aparecem. A diferença entre uma tela nota 2 e nota 3 quase nunca é "sabia a propriedade"; é controle: saber por que a regra aplicou (ou não), espaçar com ritmo, alinhar sem gambiarra de margem.
3.1 Seletores, especificidade e cascata
Os seletores que resolvem a prova:
| Seletor | Pega o quê | Exemplo de uso |
|---|---|---|
.card |
classe | o dia a dia: quase tudo |
button |
elemento | estilos de base (reset, fonte) |
.card .badge |
descendente | badge só quando está dentro de card |
.card > h3 |
filho direto | título imediato do card |
input[type="email"] |
atributo | estilizar tipos específicos |
:hover, :focus, :active, :disabled |
pseudo-classes de estado | botões e campos reagindo |
:nth-child(even) |
posição | zebrar linhas de tabela |
:not(.ativo) |
negação | todos menos o selecionado |
::before, ::after |
pseudo-elementos | ícones decorativos, setas |
::placeholder |
o texto de exemplo do input | placeholder mais suave |
Quando duas regras disputam o mesmo elemento, ganha a mais específica. A ordem de força:
style=inline (que a gente não usa)#id.classe,[atributo],:pseudo-classeelemento,::pseudo-elemento
Empatou a especificidade? Ganha a que aparece por último no arquivo (a cascata).
🛠️ Gambiarra boa: especificidade plana. Na prova, estilizar só com classes (#id fica pro JS achar elementos, não pro CSS). Com tudo no mesmo nível de força, a cascata fica previsível: a última regra ganha, ponto. Nunca usar !important: ele "resolve" agora e cobra depois, porque a próxima correção vai precisar de outro !important, e em 20 minutos o CSS vira um cabo de guerra. Se você sentiu necessidade de !important, tem uma regra mais específica escondida: acha ela no DevTools (aba Styles mostra riscado quem perdeu a disputa).
:focus não é opcional. Campo e botão precisam mostrar onde o teclado está:
button:focus-visible,
input:focus {
outline: 2px solid var(--cor-acao);
outline-offset: 2px;
}
Tirar o outline sem pôr nada no lugar (outline: none seco) é erro de acessibilidade que o módulo 06 pontua.
3.2 O modelo de caixa e a escala de espaçamento
Todo elemento é uma caixa: conteúdo, preenchimento interno (padding), borda e margem externa. O detalhe que muda tudo é como a largura é contada. No modo padrão (content-box), width: 300px + padding: 20px = caixa real de 340px, e seu layout estoura sem você entender por quê. A correção é a primeira regra de todo CSS:
/* reset mínimo de prova: as três linhas que abrem o style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Com border-box, width: 300px é 300px de verdade, padding e borda já contando. Fim das contas de padaria no meio da prova.
Espaçamento com ritmo. Espaço não se inventa a cada elemento; se escolhe de uma escala. A escala prática: múltiplos de 8px (com 4px pra ajustes finos): 4, 8, 16, 24, 32, 48. Todo padding, margin e gap da tela sai desse conjunto.
🎯 Espaçamento consistente é sinal de nível 3. Telas onde os espaços seguem um ritmo (sempre múltiplos de um valor base) parecem profissionais. Telas com espaçamento aleatório parecem amadoras, mesmo com as mesmas cores. A banca sente isso, mesmo sem saber nomear. Na seção 3.4 a escala vira variável e o ritmo vira automático.
3.3 Unidades: qual usar onde
| Unidade | O que é | Usar em |
|---|---|---|
px |
pixel fixo | borda, sombra, detalhes finos |
rem |
múltiplo da fonte raiz (16px por padrão) | fonte, padding, margin, gap |
em |
múltiplo da fonte do próprio elemento | raramente; padding interno de botão que cresce com o texto |
% |
proporção do pai | larguras fluidas |
vh / vw |
proporção da tela | min-height: 100vh pra centralizar o login |
fr |
fração do espaço livre (só no grid) | colunas de grid |
A regra de bolso: rem pra tamanhos que devem escalar junto (texto e espaçamento), px pro que é fino e fixo (borda de 1px é borda de 1px), % e fr pra layout. 1rem = 16px, 0.5rem = 8px, 1.5rem = 24px: a escala de espaçamento da seção 3.2 em rem.
Usar rem no texto respeita o usuário que aumentou a fonte do navegador. É acessibilidade de graça e é o padrão de mercado.
3.4 Custom properties: o design system em 20 linhas
Variáveis de CSS transformam decisões soltas em sistema. Declaradas no :root, valem na página inteira:
:root {
/* cores: uma de ação, neutros, três de status */
--cor-acao: #7c3aed;
--cor-acao-hover: #6d28d9;
--cor-texto: #1f2937;
--cor-texto-suave: #6b7280;
--cor-fundo: #f4f4f5;
--cor-superficie: #ffffff;
--cor-borda: #e4e4e7;
--cor-sucesso: #16a34a;
--cor-erro: #dc2626;
--cor-alerta: #d97706;
/* espaçamento: a escala da seção 3.2 */
--espaco-1: 0.25rem; /* 4px */
--espaco-2: 0.5rem; /* 8px */
--espaco-3: 1rem; /* 16px */
--espaco-4: 1.5rem; /* 24px */
--espaco-5: 2rem; /* 32px */
--espaco-6: 3rem; /* 48px */
/* acabamento */
--raio: 8px;
--sombra: 0 1px 3px rgba(0, 0, 0, 0.12);
}
.botao-primario {
background: var(--cor-acao);
padding: var(--espaco-2) var(--espaco-4);
border-radius: var(--raio);
}
O ganho é triplo:
- Consistência automática: todo botão, card e título bebe da mesma fonte. Não existe "esse verde é um pouco diferente daquele".
- Mudança global em um lugar: a banca achou o roxo escuro demais? Troca uma linha e a tela inteira acompanha. Sem variável, é caça ao
#7c3aedem 400 linhas. - Ponte com o módulo 06: os tokens de cor e espaço definidos no wireframe (design system mínimo) viram literalmente esse bloco
:root. Papel vira código em 5 minutos.
🛠️ Gambiarra boa: o :root é a primeira coisa que se escreve no Módulo C. Antes de qualquer tela: reset (3.2) + :root com tokens + estilos de base (fonte, botões, campos). Uns 15 minutos que compram consistência pelas 4 horas inteiras. E quando bater vontade de inventar uma cor nova no meio da prova, a pergunta é: "isso merece virar token?". Quase sempre a resposta é usar um que já existe.
3.5 Flexbox: alinhar num eixo
Flexbox distribui itens numa direção (linha ou coluna). É a ferramenta de todos os alinhamentos pequenos da tela:
.navbar {
display: flex;
justify-content: space-between; /* logo pra um lado, menu pro outro */
align-items: center; /* tudo alinhado na vertical */
padding: var(--espaco-3) var(--espaco-4);
}
As cinco propriedades que resolvem 95% dos casos:
flex-direction: row | columndefine o eixo principal (padrão: linha).justify-contentdistribui no eixo principal:flex-start,center,space-between,flex-end.align-itemsalinha no eixo cruzado:centeré o "alinha verticalmente" que todo mundo procura.gappõe espaço entre os itens, sem margem em ninguém.flex-wrap: wrapdeixa quebrar linha quando não cabe.
No filho, uma vale ouro: flex: 1 (cresce e ocupa o espaço livre). Campo de busca que estica com botão fixo do lado:
.barra-busca { display: flex; gap: var(--espaco-2); }
.barra-busca input { flex: 1; }
Os flex do Wedding Pass: a navbar, a linha de botões do formulário (justify-content: flex-end), o topo do card (nome à esquerda, badge à direita), e o login centralizado na tela:
.tela-login {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
🛠️ Gambiarra boa: gap em vez de margem nos filhos. Margem em item de lista cria o clássico "espaço sobrando na ponta" que precisa de :last-child pra consertar. gap só existe entre os itens. Uma propriedade, zero caso especial.
3.6 Grid: linhas e colunas ao mesmo tempo
Grid organiza em duas dimensões. É a ferramenta do layout da página e das malhas de cards:
.malha-indicadores {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4 colunas iguais */
gap: var(--espaco-3);
}
O vocabulário essencial:
grid-template-columnsdefine as colunas:repeat(4, 1fr)(4 iguais),240px 1fr(fixa + resto),2fr 1fr(proporção).gapfunciona igual ao flex.grid-column: span 2faz um item ocupar 2 colunas (o card de destaque do dashboard).grid-template-areasdesenha o layout com nomes, legível como um mapa:
.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-areas:
"menu topo"
"menu conteudo";
}
.menu { grid-area: menu; }
.topo { grid-area: topo; }
.conteudo { grid-area: conteudo; }
E a linha mais valiosa do grid pra prova:
.malha-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--espaco-3);
}
Tradução: "cria quantas colunas couberem, cada uma com no mínimo 220px, dividindo o resto igual". A malha de cards vira responsiva sem nenhuma media query: 4 colunas no monitor, 2 no tablet, 1 no celular, sozinha.
🛠️ Gambiarra boa: decorar o repeat(auto-fit, minmax(220px, 1fr)) como uma frase. É um terço da responsividade da prova em uma linha.
3.7 Flexbox ou Grid? Os dois, cada um no seu lugar
A dúvida clássica tem resposta curta: flexbox é unidimensional (o conteúdo dita o tamanho, os itens se acomodam num eixo), grid é bidimensional (o layout dita a malha, o conteúdo entra nas células). Eles não competem; se compõem: grid desenha a página, flex alinha por dentro.
O mapa de decisão do Wedding Pass:
| Situação | Ferramenta | Por quê |
|---|---|---|
| Navbar (logo + menu + usuário) | flex | uma linha, distribuir e alinhar |
| Linha de botões do form | flex | um eixo, alinhar à direita |
| Interior do card (nome + badge) | flex | um eixo |
| Centralizar o login na tela | flex | center nos dois eixos |
| Malha de indicadores do dashboard | grid | colunas e linhas de verdade |
| Página com menu lateral | grid | áreas nomeadas |
| Formulário em duas colunas | grid | campos alinhando em malha |
Pra fixar a diferença, o mesmo dashboard resolvido nos dois. Com grid (a escolha certa):
.malha-indicadores {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--espaco-3);
}
Com flex (dá, mas se nota o remendo):
.malha-indicadores { display: flex; flex-wrap: wrap; gap: var(--espaco-3); }
.malha-indicadores .card { flex: 1 1 220px; }
O flex quebra linha, mas a última linha estica os cards que sobraram (3 cards numa linha de 4 ficam mais largos que os de cima, desalinhado com a malha). O grid mantém as colunas travadas. Regra de bolso final: se você quer que as linhas e colunas se alinhem entre si, é grid; se cada item pode ter seu tamanho, é flex.
🛠️ Gambiarra boa: não sofrer com posicionamento antigo (float, margens negativas, position: absolute pra layout). Flexbox resolve os alinhamentos e Grid resolve as malhas. position: absolute fica pro que realmente flutua: badge em cima de card, toast no canto da tela (seção 6).
3.8 Hierarquia visual, cor e tipografia
O olho precisa saber pra onde olhar primeiro. Isso se cria com três alavancas:
Tamanho e peso (a escala tipográfica). Poucos tamanhos, sempre os mesmos:
| Papel | Tamanho | Peso |
|---|---|---|
Título da tela (h1) |
1.75rem | 700 |
Título de seção (h2) |
1.25rem | 600 |
Título de card (h3) |
1rem | 600 |
| Corpo | 1rem | 400 |
| Texto de apoio | 0.875rem | 400, cor suave |
| Número de indicador | 2.5rem | 700 |
E uma fonte só, a do sistema, que não precisa baixar nada e fica boa em qualquer máquina:
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
color: var(--cor-texto);
background: var(--cor-fundo);
line-height: 1.5;
}
Cor com propósito. Uma cor principal pra ação (todo botão primário, todo link: a mesma), tons neutros pro resto, e cores de status (verde confirmou, vermelho erro, amarelo atenção). Lembra da avaliação por dedução de cores do módulo 01: usar poucas cores é zero; uma paleta bem resolvida com 4+ usos coerentes é pontuação total. O :root da seção 3.4 já é essa paleta.
Contraste que se lê. Texto sobre fundo precisa de contraste mínimo 4.5:1 (texto grande, 3:1). Cinza claro sobre branco reprova. O DevTools mostra a razão de contraste ao inspecionar qualquer texto (e o módulo 06 volta nisso com o olhar de acessibilidade).
⚠️ Pega-ratão: o "arco-íris". Cada botão de uma cor, texto em cinco tamanhos aleatórios, tudo competindo por atenção. Menos é mais: uma paleta enxuta e consistente parece muito mais profissional que um monte de cor. Isso é literalmente critério pontuado.
3.9 Movimento: transition, transform e o spinner
Movimento sutil é polimento barato. Dois usos e só:
Micro-resposta em hover e foco (150 a 200ms, nunca mais que 300ms em interação):
.botao-primario {
transition: background-color 150ms ease, transform 100ms ease;
}
.botao-primario:hover { background: var(--cor-acao-hover); }
.botao-primario:active { transform: scale(0.98); }
.card { transition: box-shadow 150ms ease; }
.card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
O spinner de carregamento (vai servir os estados da seção 5.5):
@keyframes girar { to { transform: rotate(360deg); } }
.spinner {
width: 24px;
height: 24px;
border: 3px solid var(--cor-borda);
border-top-color: var(--cor-acao);
border-radius: 50%;
animation: girar 0.8s linear infinite;
}
🛠️ Gambiarra boa: transition de 150ms nos botões e cards é o acabamento com melhor custo-benefício da prova: quatro linhas de CSS e a interface inteira parece "viva". Já animação chamativa (coisas voando, pulsando, girando sem motivo) joga contra: distrai e parece enfeite de quem não terminou o resto.
4. Responsividade e mobile-first
O descritivo pede interface responsiva: que funcione bem em telas de tamanhos diferentes, do celular ao monitor. A UC15 inteira é sobre isso.
Mobile-first na prática: o CSS base descreve a tela pequena (uma coluna, tudo empilhado), e as media queries adicionam colunas conforme o espaço aparece:
/* base: celular, uma coluna, sem media query nenhuma */
.malha-indicadores {
display: grid;
gap: var(--espaco-3);
}
/* tablet: duas colunas */
@media (min-width: 768px) {
.malha-indicadores { grid-template-columns: repeat(2, 1fr); }
}
/* desktop: quatro colunas */
@media (min-width: 1024px) {
.malha-indicadores { grid-template-columns: repeat(4, 1fr); }
}
Por que essa ordem (e não o contrário com max-width): a versão simples é a base, e complexidade só entra quando cabe. Esquecer um breakpoint deixa a tela empilhada porém funcional, nunca quebrada. Dois breakpoints bastam pra prova: 768px e 1024px.
O kit completo da responsividade:
- Meta viewport no HTML (seção 2.1). Sem ela, nada disso liga.
- Media queries
min-widthnos dois breakpoints. repeat(auto-fit, minmax(...))nas malhas de cards (seção 3.6): responsivo sem media query.- Imagem que não estoura:
img { max-width: 100%; height: auto; }no reset. - Tabela em tela pequena: tabela não espreme; ela rola pro lado dentro de um invólucro:
.tabela-wrapper { overflow-x: auto; }
<div class="tabela-wrapper">
<table> ... </table>
</div>
(Alternativa mais elegante: em telas pequenas, mostrar cards em vez da tabela. Mais trabalho; o wrapper resolve em 30 segundos.)
- Alvos de toque: botão com
paddinggeneroso (mínimo ~44px de altura clicável) pra dedo, não só pra mouse.
🎯 Foco de prova: responsividade é ponto que a banca testa fácil, é só apertar a janela do navegador. Se a tela quebra (texto sai pra fora, botão some, tabela estoura a página), cai nota na hora. Testar em tela pequena durante o desenvolvimento, não no fim.
🛠️ Gambiarra boa: DevTools com o modo device (Ctrl+Shift+M) aberto num canto enquanto desenvolve, simulando um celular de ~375px. Cada tela nova, um olhar de 10 segundos: cabe, rola, aperta? Consertar na hora custa um minuto; descobrir na apresentação custa nota.
5. JavaScript: do clique até a API
Aqui os dois mundos se juntam. O front pede dados pra API (módulo 04) e mostra na tela. O descritivo diz assíncrono: a página não trava enquanto espera a resposta.
5.1 DOM e eventos: a rampa
O DOM é a página viva na memória do navegador; o JS lê e mexe nela. O ciclo de toda tela é: achar elemento → reagir a evento → atualizar a tela.
// achar elementos (uma vez, no topo do arquivo)
const form = document.querySelector('#form-convidado');
const lista = document.querySelector('#lista-convidados');
// reagir ao envio do formulário
form.addEventListener('submit', (evento) => {
evento.preventDefault(); // impede o recarregamento da página
const dados = Object.fromEntries(new FormData(form));
// dados = { nome: '...', email: '...', telefone: '...', acompanhantes: '0' }
salvarConvidado(dados);
});
querySelectorusa a mesma sintaxe dos seletores CSS (#id,.classe).querySelectorAlldevolve todos.FormData+Object.fromEntrieslê o formulário inteiro de uma vez (por isso onameem cada campo na seção 2.3). Sem catar valor input por input.evento.preventDefault()no submit é obrigatório: sem ele o navegador recarrega a página no meio da requisição.
⚠️ Pega-ratão: esquecer o preventDefault. O sintoma é fantasmagórico: a página "pisca", a requisição morre pela metade, às vezes o dado até salva mas a tela não mostra. Se a tela pisca ao enviar, é isso.
Pra escrever na tela, o caminho da prova é gerar HTML com template literals (crases) e innerHTML. Com uma regra de segurança inegociável:
// escapa texto vindo do usuário antes de pôr no HTML
function escapar(texto) {
const div = document.createElement('div');
div.textContent = texto ?? '';
return div.innerHTML;
}
lista.innerHTML = `<h3>${escapar(convidado.nome)}</h3>`;
⚠️ Pega-ratão: XSS, a irmã do SQL injection. innerHTML com dado do usuário sem escapar executa o que vier: um convidado cadastrado como <script>...</script> roda na tela da recepção. Mesmo raciocínio do módulo 04 (§6): dado de usuário nunca é código. Lá, prepared statement; aqui, escapar() em todo campo que veio de gente.
5.2 Síncrono, assíncrono e promises
Buscar dados na API leva tempo (rede). Se o JS esperasse parado, a página congelaria: sem clique, sem rolagem, sem digitação. Por isso toda operação de rede é assíncrona: o JS dispara o pedido, segue a vida, e trata a resposta quando ela chegar.
O objeto que representa "uma resposta que ainda vai chegar" é a Promise. Ela tem três estados: pendente (esperando), resolvida (deu certo, tem valor) ou rejeitada (deu erro). Dá pra consumir de dois jeitos:
// jeito antigo: encadear .then()
fetch(url)
.then((resposta) => resposta.json())
.then((dados) => mostrar(dados))
.catch((erro) => avisar(erro.message));
// jeito da prova: async/await (lê de cima pra baixo, como código normal)
async function carregar() {
try {
const resposta = await fetch(url);
const dados = await resposta.json();
mostrar(dados);
} catch (erro) {
avisar(erro.message);
}
}
São equivalentes. O async/await ganha porque parece código síncrono (fácil de ler e de debugar) e o erro cai num try/catch normal. Regras do jogo: await só dentro de função async, e todo await esquecido devolve uma Promise em vez do valor (se aparecer [object Promise] na tela, faltou um await).
5.3 A anatomia do fetch
fetch é a função do navegador pra chamar a API. Os dois formatos que cobrem a prova inteira, batendo nas rotas que vocês escreveram no módulo 04 (§2.4):
GET (buscar dados):
const resposta = await fetch('http://localhost:3000/convidados', {
headers: { 'Authorization': `Bearer ${token}` },
});
const convidados = await resposta.json();
POST/PUT (enviar dados):
const resposta = await fetch('http://localhost:3000/convidados', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ nome, email, telefone, acompanhantes }),
});
As quatro peças: URL (a rota do mapa de endpoints), method (GET é o padrão; POST/PUT/DELETE se declara), headers (o Content-Type avisando que vai JSON + o token do módulo 04 §8), body (o objeto passado por JSON.stringify, nunca cru).
⚠️ Os três esquecimentos do fetch (decorar como checklist mental):
1. Esqueceu Content-Type: application/json → o back recebe body vazio. É exatamente o sintoma req.body undefined do módulo 04: agora você conhece os dois lados dele.
2. Esqueceu JSON.stringify → o body vira a string [object Object].
3. Esqueceu o header Authorization → 401 em tudo, mesmo logada.
E o quarto primo: se o navegador reclamar de CORS no console, o problema é no back (módulo 04 §10), não no front. Não perder 40 minutos mexendo no fetch.
5.4 Tratamento de erro: o que o fetch NÃO faz por você
A pegadinha mais importante do módulo: o fetch só rejeita a Promise em erro de rede (API fora do ar, sem internet). Resposta 404, 409, 500? Pra ele, "deu certo": chegou uma resposta. Quem não sabe disso escreve try/catch, acha que está protegida, e os erros do servidor passam batidos pela tela.
A verificação é sua, via resposta.ok (verdadeiro só pra status 2xx):
async function buscarConvidados() {
const resposta = await fetch(url, { headers });
if (!resposta.ok) {
// lê o corpo de erro padronizado do módulo 04: { erro, detalhes }
const corpo = await resposta.json().catch(() => ({}));
throw new Error(corpo.erro || `Erro ${resposta.status}`);
}
return resposta.json();
}
Dois caminhos de falha, dois comportamentos:
- Erro HTTP (4xx/5xx): a resposta chega,
resposta.oké falso, você lê o{ erro }que o back mandou e lança com a mensagem certa. - Erro de rede: o
fetchrejeita sozinho comTypeError: Failed to fetch. Cai direto nocatch. (Ler essa mensagem em inglês sem sustos é treino do módulo 09:failed to fetch= "falhou em buscar" = a API não respondeu. Primeira suspeita: o servidor está rodando?)
🎯 Foco de prova: a banca vai derrubar o caminho feliz: parar a API, cadastrar duplicado, buscar id inexistente (é literalmente o roteiro de Postman do módulo 04 §15, agora visto da tela). Front nota 3 é o que mostra mensagem clara nesses momentos em vez de tela travada em "Carregando..." pra sempre.
5.5 Os quatro estados de toda tela
Toda tela que consome API vive em um de quatro estados, e os quatro precisam existir no código:
- Carregando: esperando a resposta (spinner ou "Carregando...").
- Sucesso: chegaram dados, mostra.
- Vazio: deu certo, mas não tem nada ("Nenhum convidado cadastrado ainda"). Sucesso sem tratamento de vazio vira tela em branco confusa.
- Erro: falhou, avisa com mensagem legível e, ideal, um jeito de tentar de novo.
Os quatro implementados, no padrão que serve pra qualquer lista da prova:
async function carregarConvidados() {
lista.innerHTML = '<div class="spinner"></div>'; // 1. carregando
try {
const convidados = await api('/convidados'); // seção 5.6
if (convidados.length === 0) { // 3. vazio
lista.innerHTML = '<p class="estado-vazio">Nenhum convidado cadastrado ainda.</p>';
return;
}
lista.innerHTML = convidados.map(cardConvidado).join(''); // 2. sucesso
} catch (erro) { // 4. erro
lista.innerHTML = `
<p class="estado-erro">
Não deu pra carregar a lista: ${escapar(erro.message)}
<button type="button" onclick="carregarConvidados()">Tentar de novo</button>
</p>`;
}
}
🎯 Os estados que todo mundo esquece. Fazer só o caso de sucesso é nível 2. Tratar os quatro é nível 3. E é fácil esquecer o de erro porque, quando você testa, dá tudo certo. O teste honesto: desligar a API e abrir cada tela. Toda tela precisa reagir com dignidade. 30 segundos de teste, e é exatamente o que a banca faz.
5.6 O helper api(): escreva o fetch uma vez
Repetir aquele fetch de 15 linhas em cada chamada é convite pra esquecer um header e criar bug. O padrão profissional (e o maior economizador de tempo do Módulo C) é centralizar tudo num helper:
// js/api.js: toda a conversa com o back passa por aqui
const API_URL = 'http://localhost:3000';
async function api(rota, opcoes = {}) {
const token = localStorage.getItem('token');
const resposta = await fetch(API_URL + rota, {
...opcoes,
headers: {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
},
body: opcoes.body ? JSON.stringify(opcoes.body) : undefined,
});
// 401 = não autenticado (sem token, vencido ou inválido) → volta pro login
if (resposta.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('usuario');
window.location.href = 'index.html';
return new Promise(() => {}); // página vai trocar; ninguém mais precisa da resposta
}
const corpo = resposta.status === 204 ? null : await resposta.json().catch(() => null);
if (!resposta.ok) {
// formato padronizado do módulo 04 (§12): { erro, detalhes }
const erro = new Error(corpo?.erro || `Erro ${resposta.status}`);
erro.status = resposta.status;
erro.detalhes = corpo?.detalhes;
throw erro;
}
return corpo;
}
E cada tela vira uma linha por chamada:
const convidados = await api('/convidados');
const novo = await api('/convidados', { method: 'POST', body: dados });
await api(`/convidados/${id}`, { method: 'PUT', body: dados });
await api(`/convidados/${id}/checkin`, { method: 'POST' });
const capacidade = await api(`/casamentos/${casamentoId}/capacidade`);
O que esse helper resolve de uma vez por todas:
- Token automático: lê do
localStorage(a decisão do módulo 04 §8.4) e anexa em toda chamada. Impossível esquecer o header. - 401 tratado num lugar só: token venceu em qualquer tela → limpa e manda pro login. E repara a divisão de trabalho combinada no módulo 04: 401 volta pro login; 403 não (o 403 significa "logada, mas sem permissão": ele vira
throw, e a tela mostra "sem permissão pra isso"). - Erro do back legível: o
{ erro, detalhes }padronizado do módulo 04 viraerro.messageeerro.detalhesprontos pra tela. - Base URL num lugar só: a prova pediu outra porta? Uma linha.
🛠️ Gambiarra boa: o api.js se escreve nos primeiros 10 minutos do Módulo C, junto com o :root do CSS. É o par de fundações. Vale treinar até sair de memória: são ~30 linhas que transformam toda integração do resto da prova em chamadas de uma linha.
6. Feedback visual: a interface conversando
O descritivo pede, no check-in, feedback visual imediato. Isso é um princípio geral: toda ação do usuário merece uma resposta visível. Três padrões implementados cobrem a prova inteira:
1. O toast (aviso que aparece e some). Serve pra confirmar qualquer ação: salvou, editou, apagou.
function avisar(mensagem, tipo = 'sucesso') {
const aviso = document.createElement('div');
aviso.className = `toast toast-${tipo}`;
aviso.textContent = mensagem;
document.body.append(aviso);
setTimeout(() => aviso.remove(), 3500);
}
// uso: avisar('Convidado salvo!'); avisar(erro.message, 'erro');
.toast {
position: fixed;
top: var(--espaco-3);
right: var(--espaco-3);
padding: var(--espaco-3) var(--espaco-4);
background: var(--cor-superficie);
border-left: 4px solid var(--cor-sucesso);
border-radius: var(--raio);
box-shadow: var(--sombra);
}
.toast-erro { border-left-color: var(--cor-erro); }
2. O botão que mostra que está trabalhando. Durante o await, o botão trava e avisa:
form.addEventListener('submit', async (evento) => {
evento.preventDefault();
const botao = form.querySelector('button[type="submit"]');
botao.disabled = true;
botao.textContent = 'Salvando...';
try {
await api('/convidados', { method: 'POST', body: lerFormulario() });
avisar('Convidado salvo!');
form.reset();
} catch (erro) {
avisar(erro.message, 'erro');
} finally {
botao.disabled = false;
botao.textContent = 'Salvar convidado';
}
});
⚠️ Pega-ratão: o clique duplo. Botão sem disabled durante o await deixa o usuário clicar duas vezes e cadastrar dois convidados iguais (ou dois check-ins: a corrida do módulo 04 §11.2, provocada pela própria tela). O banco segura com a UNIQUE, mas a experiência fica feia e a banca vê. disabled = true na primeira linha, finally devolvendo, sempre.
3. O resultado gigante do check-in. A tela vitrine merece o feedback mais visível do sistema:
<div id="resultado-checkin" class="resultado" hidden></div>
async function fazerCheckin(convidadoId, nome) {
const painel = document.querySelector('#resultado-checkin');
try {
await api(`/convidados/${convidadoId}/checkin`, { method: 'POST' });
painel.className = 'resultado resultado-ok';
painel.textContent = `✔ Bem-vinda, ${nome}!`;
} catch (erro) {
// o 409 do módulo 04: check-in duplicado
painel.className = 'resultado resultado-erro';
painel.textContent = `✖ ${erro.message}`;
}
painel.hidden = false;
}
.resultado {
padding: var(--espaco-5);
border-radius: var(--raio);
font-size: 2rem;
font-weight: 700;
text-align: center;
color: #fff;
}
.resultado-ok { background: var(--cor-sucesso); }
.resultado-erro { background: var(--cor-erro); }
🎯 O check-in ágil é vitrine. O descritivo destaca "check-in ágil com feedback visual". É uma das telas que a banca mais olha. Feedback grande, colorido e imediato ali não é enfeite, é o critério: uma recepcionista real precisa bater o olho de longe e saber se pode deixar entrar. Verde gigante com o nome, ou vermelho gigante com o motivo. Fonte de 2rem pra cima, sem medo.
7. As telas da prova, com cara de nota 3
O descritivo nomeia telas específicas. O que cada uma cobra e como se resolve:
7.1 Login: a porta
Simples, mas se não funciona, nada funciona. O fluxo completo:
form.addEventListener('submit', async (evento) => {
evento.preventDefault();
try {
const { token, usuario } = await api('/auth/login', {
method: 'POST',
body: Object.fromEntries(new FormData(form)),
});
localStorage.setItem('token', token);
localStorage.setItem('usuario', JSON.stringify(usuario));
// cada perfil cai direto na sua tela de trabalho
window.location.href = usuario.perfil === 'recepcao' ? 'checkin.html' : 'dashboard.html';
} catch {
mostrarErro('E-mail ou senha inválidos'); // mensagem no form, nunca alert()
}
});
A resposta do login é a que o módulo 04 (§8.2) definiu: { token, usuario: { id, nome, perfil } }. Guardar os dois no localStorage: o token pras chamadas, o usuário pra tela saber o nome e o perfil sem decodificar nada.
Detalhes que somam: input de senha com type="password", erro mostrado dentro do form perto dos campos, botão em "Entrando..." durante o await, e o redirecionamento por perfil (a recepção cai direto no check-in, que é onde ela vive).
7.2 Dashboard gerencial: números que respiram
Indicadores de lotação em tempo real: quantos confirmaram, quantos já entraram, quanto da capacidade foi usada, e o alerta de 90%. Os números vêm da rota de capacidade que vocês escreveram no módulo 04 (§11.1); o front só apresenta:
async function carregarIndicadores() {
const dados = await api(`/casamentos/${casamentoId}/capacidade`);
// os nomes dos campos são os que a rota de vocês devolve; o padrão do treino:
// { capacidade_total, confirmados, presentes, percentual, alerta }
document.querySelector('#num-confirmados').textContent = dados.confirmados;
document.querySelector('#num-presentes').textContent = dados.presentes;
document.querySelector('#barra-ocupacao').style.width = `${dados.percentual}%`;
document.querySelector('#alerta-90').hidden = !dados.alerta;
}
carregarIndicadores();
setInterval(carregarIndicadores, 15000); // atualiza sozinho a cada 15s
A malha de cards usa o grid da seção 3.6, os números em 2.5rem peso 700 (a escala da 3.8), e o alerta de 90% é um banner que só perde o hidden quando a rota mandar alerta: true: fundo --cor-alerta, texto direto ("⚠ Capacidade acima de 90%").
🎯 "Tempo real" na prática. Não precisa de tecnologia complicada. Basta a tela buscar o número atualizado quando abre e quando algo muda (recarregar os indicadores depois de cada check-in confirmado). O setInterval de 15s é a gambiarra honesta que fecha o resto. O que a banca quer ver é o número refletindo a realidade do banco: ela faz um check-in e olha o dashboard; se o número andou, é tempo real o suficiente. WebSocket é bala de canhão pra esse pardal.
7.3 Check-in ágil: a vitrine
O fluxo da recepcionista tem que caber em segundos: digitar um pedaço do nome (ou o código do convite), ver o convidado, confirmar, feedback gigante. Componentes já construídos nesse módulo: busca com a lista filtrando enquanto digita (seção 9.3), botão de confirmar com disabled durante o await (seção 6), painel verde/vermelho de resultado (seção 6). O erro 409 de dupla entrada vem do back com a mensagem pronta (erro.message), o front só exibe no vermelho.
Um detalhe de agilidade que a banca sente: o campo de busca já nasce focado (autofocus no input, ou campo.focus() ao carregar). A recepcionista não pega o mouse.
7.4 Os três perfis na interface
Os perfis são os do ENUM do módulo 03: admin, organizador e recepcao. Cada um enxerga um sistema diferente:
| Perfil | O que vê |
|---|---|
admin |
tudo: dashboard, convidados (com excluir), mesas, check-in |
organizador |
dashboard, convidados e mesas (sem excluir convidado) |
recepcao |
busca + check-in (a tela dela é o sistema inteiro) |
Como esconder e mostrar é a seção 8. O que importa aqui: a experiência de cada perfil precisa fazer sentido sozinha: a recepção não deve nem ver menu pra telas que vão dar 403.
8. Controle de acesso na interface
Duas camadas no front, as duas em js/auth.js, importado por toda página interna:
1. A guarda de página (as primeiras linhas do arquivo): sem token, nem carrega a tela:
// js/auth.js
if (!localStorage.getItem('token')) {
window.location.href = 'index.html';
}
const usuario = JSON.parse(localStorage.getItem('usuario') || 'null');
2. Render condicional por perfil: o HTML marca o que é restrito com classes, e uma linha de JS limpa o que não é do perfil:
<button class="botao-perigo so-admin" data-acao="excluir">Excluir</button>
<a href="dashboard.html" class="so-gestao">Dashboard</a>
if (usuario.perfil !== 'admin') {
document.querySelectorAll('.so-admin').forEach((el) => el.remove());
}
if (usuario.perfil === 'recepcao') {
document.querySelectorAll('.so-gestao').forEach((el) => el.remove());
}
remove() em vez de esconder com CSS: o elemento sai do DOM de verdade (não fica um botão "invisível" que um Ctrl+F no inspetor acha em 5 segundos).
⚠️ Pega-ratão (o mesmo de sempre, do outro lado): achar que esconder o botão é segurança. Não é. Esconder é experiência (não confundir o usuário com opções que ele não pode usar). A segurança é o back barrar com 403 (módulo 04 §9), porque o token dá pra editar, o JS dá pra desligar e a rota dá pra chamar por Postman. Os dois trabalham juntos: front esconde pra ficar limpo, back barra pra ficar seguro. Na apresentação, falar essa frase vale ponto de julgamento.
9. SPA, componentes e estado
O descritivo cita Full Stack / SPA (Single Page Application): navegação sem recarregar a página, trocando só o conteúdo. Antes da arquitetura, os dois conceitos que valem em qualquer front:
9.1 Componente: a função que rende HTML
Um componente, em vanilla JS, é só uma função que recebe dados e devolve HTML:
function cardConvidado(convidado) {
const status = convidado.status_convite ?? 'pendente';
return `
<article class="card" data-id="${convidado.id}">
<div class="card-topo">
<h3>${escapar(convidado.nome)}</h3>
<span class="badge badge-${status}">${status}</span>
</div>
<p class="texto-suave">
Mesa ${convidado.mesa_numero ?? 'sem mesa'} · ${convidado.acompanhantes} acompanhante(s)
</p>
<div class="acoes">
<button type="button" data-acao="editar">Editar</button>
<button type="button" class="so-admin" data-acao="excluir">Excluir</button>
</div>
</article>`;
}
// uma lista inteira é um map + join:
lista.innerHTML = convidados.map(cardConvidado).join('');
🛠️ Gambiarra boa: componentizar economiza tempo e nota. O cardConvidado aparece na lista, no resultado da busca do check-in e no detalhe. Escrito uma vez. Menos código, menos bug, e consistência visual automática (todos os cards idênticos = tela profissional). É trabalho no começo que paga no meio da prova, quando o tempo aperta.
Os cliques nos botões dos cards usam delegação: um listener na lista resolve todos os cards, inclusive os que ainda vão nascer:
lista.addEventListener('click', (evento) => {
const botao = evento.target.closest('[data-acao]');
if (!botao) return;
const id = botao.closest('.card').dataset.id;
if (botao.dataset.acao === 'editar') abrirEdicao(id);
if (botao.dataset.acao === 'excluir') confirmarExclusao(id);
});
(Listener em cada card se perde toda vez que o innerHTML re-renderiza. Delegação na lista sobrevive.)
9.2 Estado: os dados que a tela mostra
Estado é o que a tela está exibindo agora: a lista de convidados carregada, o texto digitado na busca. O padrão que organiza qualquer tela da prova: um objeto de estado + uma função render() que desenha a partir dele:
const estado = { convidados: [], busca: '' };
function render() {
const filtrados = estado.convidados.filter((c) =>
c.nome.toLowerCase().includes(estado.busca.toLowerCase())
);
lista.innerHTML = filtrados.length
? filtrados.map(cardConvidado).join('')
: '<p class="estado-vazio">Nenhum convidado encontrado.</p>';
}
9.3 A regra de ouro: mudou o estado, chama o render
// carregou da API → estado muda → render
estado.convidados = await api('/convidados');
render();
// digitou na busca → estado muda → render (busca instantânea, sem ir ao servidor)
campoBusca.addEventListener('input', () => {
estado.busca = campoBusca.value;
render();
});
A busca local filtrando a cada tecla dá a sensação de agilidade que o check-in pede. (Pra listas grandes, a API do módulo 04 §13 já aceita ?busca=; com o volume da prova, filtrar o que já veio é mais rápido e mais simples.)
9.4 E o SPA?
Multi-página (um HTML por tela) é o caminho recomendado: mais simples, F5 sempre funciona, menos risco. Se a prova exigir SPA explícito, a versão vanilla honesta: um HTML só com uma <section> por tela, navegação mostrando uma e escondendo as outras:
function navegar(tela) {
document.querySelectorAll('main > section').forEach((s) => (s.hidden = true));
document.querySelector(`#tela-${tela}`).hidden = false;
}
O resto (componentes, estado, render) é idêntico. É por isso que essa seção veio antes: quem domina componente + estado troca de arquitetura em 20 minutos.
10. Validação no client: resposta imediata
O sistema valida em três camadas, e cada uma tem um papel:
- Atributos HTML (
required,type,minlength,min/max): de graça, sem JS. - JS antes do fetch: mensagens melhores, no lugar certo, na hora.
- O servidor (módulo 04 §5): a única que é segurança. As duas primeiras são experiência.
⚠️ Pega-ratão conceitual (de novo, porque a banca pergunta): validação só no front é porta destrancada, o Postman passa reto por ela. O front valida pra responder na hora sem viagem ao servidor; o back valida pra proteger. As duas existem, com mensagens parecidas, e você sabe explicar por quê.
A validação de JS no submit, usando os espaços de erro do form da seção 2.3:
function validarConvidado(dados) {
const erros = {};
if (!dados.nome || dados.nome.trim().length < 3) {
erros.nome = 'Informe o nome completo (mínimo 3 letras)';
}
if (dados.email && !dados.email.includes('@')) {
erros.email = 'E-mail inválido';
}
const acomp = Number(dados.acompanhantes);
if (Number.isNaN(acomp) || acomp < 0 || acomp > 5) {
erros.acompanhantes = 'Acompanhantes: entre 0 e 5';
}
return erros;
}
form.addEventListener('submit', async (evento) => {
evento.preventDefault();
const dados = Object.fromEntries(new FormData(form));
const erros = validarConvidado(dados);
mostrarErrosNoForm(erros); // escreve em cada .campo-erro
if (Object.keys(erros).length > 0) return; // não chama a API com dado inválido
// ... segue pro api() com botão travado (seção 6)
});
function mostrarErrosNoForm(erros) {
form.querySelectorAll('.campo').forEach((campo) => {
const input = campo.querySelector('input, select');
const aviso = campo.querySelector('.campo-erro');
const mensagem = erros[input.name];
aviso.textContent = mensagem ?? '';
aviso.hidden = !mensagem;
input.classList.toggle('campo-invalido', Boolean(mensagem));
});
}
.campo-erro { color: var(--cor-erro); font-size: 0.875rem; }
.campo-invalido { border-color: var(--cor-erro); }
🎯 Onde a nota mora: mensagem perto do campo, em português claro, dizendo o que fazer ("Informe o nome completo", não "erro no formulário"). E se o back devolver detalhes no erro (o formato do módulo 04 §12), mostrar essas mensagens também: o helper api() já as entrega em erro.detalhes.
E as máscaras de telefone e afins? São a cereja da lapidação e têm módulo próprio: o padrão formata-mostra/guarda-limpo está no módulo 08, com código pronto. Aqui, o essencial: máscara é conforto visual; validação é outra coisa; e campo mascarado se manda limpo pra API.
11. Peso da página: otimização que dá pra ver
A UC15 cobra "otimização para dispositivos móveis". Na prova isso se traduz em: a página abre rápido e nada trava. O checklist enxuto:
- Imagem é o vilão número 1. Foto de fundo de 4000px no login = página lenta na frente da banca. Antes de usar: redimensionar pro tamanho real de exibição e comprimir (Squoosh faz os dois no navegador). Formato: foto em JPG/WebP, ícone e logo em SVG ou PNG pequeno.
loading="lazy"em imagem que aparece em lista (só carrega quando entra na tela): uma palavra no HTML.- Fonte de sistema (seção 3.8): zero download de fonte. A página mais rápida é a que não pede.
- Sem biblioteca desnecessária: cada framework/plugin que "ajudaria" custa peso, risco e tempo de aprender na hora. Vanilla resolve a prova inteira: é a decisão que esse módulo inteiro treina.
- Minificação (saber explicar): remover espaços e comentários do CSS/JS pra reduzir bytes. Em produção real, ferramenta de build; na prova, não vale o tempo. O que vale: um CSS e poucos JS (menos requisições), e saber dizer o que minificação é se a banca perguntar (a UC15 cita).
- Conferir no DevTools, aba Network: recarregar e olhar o total de KB e o tempo. Se a página passa de uns poucos MB, tem imagem gorda escondida.
🛠️ Gambiarra boa: a otimização com melhor retorno na prova são imagens tratadas + zero dependência. Se a página abre instantânea no notebook da banca, otimização cumprida. Lighthouse (no próprio DevTools) dá um relatório com nota se sobrar tempo de curiosidade no treino.
12. Autoavaliação do módulo
- Qual o papel de HTML, CSS e JavaScript, e por que separar os três (em papéis e em arquivos)?
- O que a
meta viewportfaz? O que acontece com as media queries sem ela? - Por que
buttonde verdade ganha dedivcom clique? Cita duas coisas que a tag dá de graça. - O que
label for+idfaz e por que é o ponto de acessibilidade mais barato do form? - Numa disputa entre
.card .tituloe.titulo, quem ganha e por quê? Qual a estratégia de especificidade da prova e por que!importanté armadilha? - O que
box-sizing: border-boxmuda na conta da largura? Quais as três linhas do reset mínimo? remoupx: qual usar em fonte e espaçamento, e por quê?- Por que o
:rootcom custom properties é a primeira coisa que se escreve no CSS da prova? Que ligação ele tem com o módulo 06? - Flexbox ou grid: qual o critério de escolha? Dá um exemplo do Wedding Pass de cada.
- O que
repeat(auto-fit, minmax(220px, 1fr))faz e por que economiza media query? - O que é mobile-first e quais os dois breakpoints do treino? Como uma tabela sobrevive à tela pequena?
- O que o
fetchNÃO faz quando o servidor responde 500? O que éresposta.oke por que checar? - Quais os quatro estados de uma tela que consome API? Qual o teste de 30 segundos que revela se estão tratados?
- Cita três problemas que o helper
api()resolve de uma vez. Qual a diferença de tratamento entre 401 e 403 no front? - Por que
innerHTMLcom dado de usuário sem escapar é perigoso? Qual o paralelo com o módulo 04? - Por que o botão trava (
disabled) durante oawait? Que bug do módulo 04 isso evita de provocar? - Se o back já valida, por que validar no front também? O que cada camada garante?
- De onde vem o número de "lotação em tempo real" e o que "tempo real" exige de fato na prova?
- Duas otimizações de página com melhor retorno na prova: quais e por quê?
Front no lugar? Agora a gente aprofunda no que faz uma interface ser boa de usar, não só bonita: 06 — UX e interface.
Referências do módulo
Bibliografia do plano de curso (UC12 e UC15): - SILVA, Maurício Samy. CSS Grid Layout. Novatec. - DUCKETT, Jon. HTML e CSS: projete e construa websites. Alta Books. - ZEMEL, Tárcio. CSS eficiente: técnicas e ferramentas que fazem a diferença nos seus estilos. Casa do Código. - LOPES, Sérgio. A Web Mobile: programe para um mundo de muitos dispositivos. Casa do Código. - SOUZA, Weiller. Bootstrap 4: conheça o framework front-end mais utilizado no mundo (leitura de contexto; a prova é vanilla).
Documentação e mercado: - MDN: conceitos básicos de flexbox - MDN: grids CSS - MDN: usando propriedades personalizadas (custom properties) - MDN: media queries - MDN: usando a Fetch API - MDN: async e await - web.dev: tratamento de erros com a Fetch API - Dmitri Pavlutin: How to Use Fetch with async/await - Alura: quando utilizar Grid e quando utilizar Flexbox - Alura: praticando CSS com Grid e Flexbox - Le Wagon: CSS Grid ou Flexbox - Impacta: Flexbox ou CSS Grid, quando utilizar cada ferramenta