Atividade Eventos — VibeStore
Disciplina: Programação Web 2
Assunto: Eventos e Interatividade
Professor: Me. Danilo Bueno
Grupos: duplas
Duração: 4 aulas de 50 minutos (em sala)
Importante
Antes de começar a atividade leia o material de apoio sobre JavaScript!! É imprenscindível que você leia antes de começar a codar, pois a atividade é justamente para praticar os conceitos apresentados no material.
Contextualização
A VibeStore é uma loja online de produtos para desenvolvedores: camisetas, moletons e acessórios com estampas do universo dev. O time de design entregou o HTML e o CSS da vitrine, mas a página está completamente estática — nenhum botão funciona.
A dupla foi contratada como time de frontend para dar vida à interface: o menu hamburguer precisa abrir e fechar, as abas devem carregar os produtos de cada categoria e o modal tem que exibir os detalhes do item selecionado.
Como começar
Crie três arquivos na mesma pasta:
index.html — copie o código abaixo e não modifique este arquivo:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<title>VibeStore</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<!-- Cabeçalho com menu hamburguer -->
<header>
<span class="logo">VibeStore</span>
<button id="btn-menu" aria-label="Abrir menu">☰</button>
<nav id="menu">
<a href="#" class="link-menu">Início</a>
<a href="#" class="link-menu">Sobre</a>
<a href="#" class="link-menu">Contato</a>
</nav>
</header>
<!-- Abas de categorias -->
<main>
<div id="abas">
<button class="btn-aba ativo" data-aba="camisetas">Camisetas</button>
<button class="btn-aba" data-aba="moletons">Moletons</button>
<button class="btn-aba" data-aba="acessorios">Acessórios</button>
</div>
<div id="painel-camisetas" class="painel"></div>
<div id="painel-moletons" class="painel escondido"></div>
<div id="painel-acessorios" class="painel escondido"></div>
</main>
<!-- Modal de detalhes -->
<div id="modal" class="escondido">
<div class="modal-overlay"></div>
<div class="modal-conteudo">
<button id="btn-fechar-modal">✕</button>
<h2 id="modal-nome"></h2>
<p id="modal-descricao"></p>
<p id="modal-preco"></p>
</div>
</div>
<script src="vibestore.js"></script>
</body>
</html>
styles.css — copie o código abaixo e não modifique este arquivo:
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; }
/* Header */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: #1a1a2e;
color: white;
}
.logo { font-weight: bold; font-size: 1.2rem; }
#btn-menu { background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; }
/* Menu — oculto por padrão, visível quando .ativo */
#menu {
display: none;
flex-direction: column;
position: absolute;
top: 56px;
right: 0;
background: #16213e;
min-width: 160px;
padding: 0.5rem 0;
z-index: 100;
}
#menu.ativo { display: flex; }
#menu a { color: white; text-decoration: none; padding: 0.75rem 1.5rem; }
#menu a:hover { background: #0f3460; }
/* Abas */
main { padding: 1.5rem; }
#abas { display: flex; gap: 0; border-bottom: 2px solid #1a1a2e; margin-bottom: 1.5rem; }
.btn-aba {
padding: 0.6rem 1.5rem;
border: 2px solid transparent;
border-radius: 6px 6px 0 0;
background: #f0f0f0;
cursor: pointer;
font-size: 0.9rem;
color: #666;
position: relative;
bottom: -2px;
transition: background 0.15s;
}
.btn-aba:hover { background: #e0e0e0; }
.btn-aba.ativo {
background: white;
border-color: #1a1a2e;
border-bottom-color: white;
color: #1a1a2e;
font-weight: 600;
}
/* Painéis */
.painel { display: flex; flex-wrap: wrap; gap: 1rem; }
.escondido { display: none !important; }
/* Cards */
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
width: 200px;
}
.card h3 { font-size: 1rem; margin-bottom: 0.5rem; }
.card p { font-size: 0.875rem; color: #555; margin-bottom: 0.75rem; }
.btn-detalhes {
width: 100%;
padding: 0.4rem;
background: #1a1a2e;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* Modal */
#modal {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.5);
}
.modal-conteudo {
position: relative;
background: white;
border-radius: 8px;
padding: 2rem;
max-width: 400px;
width: 90%;
z-index: 1;
}
#btn-fechar-modal {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
}
vibestore.js — copie o código abaixo. Todo o código da atividade vai abaixo do objeto produtos:
const produtos = {
camisetas: [
{ id: 1, nome: "Camiseta Dev Clássica", preco: 59.90, descricao: "100% algodão. Estampa minimalista para o dia a dia." },
{ id: 2, nome: "Camiseta Open Source", preco: 64.90, descricao: "Para quem compartilha código, ideias e café." },
{ id: 3, nome: "Camiseta Semicolon", preco: 54.90, descricao: "Um tributo ao símbolo mais incompreendido da programação." },
],
moletons: [
{ id: 4, nome: "Moletom Dark Mode", preco: 129.90, descricao: "Fundo escuro, igual ao seu tema favorito." },
{ id: 5, nome: "Moletom Pull Request", preco: 119.90, descricao: "Porque todo bom merge merece uma celebração." },
],
acessorios: [
{ id: 6, nome: "Caneca Terminal", preco: 39.90, descricao: "Começa o dia com café e um bom comando." },
{ id: 7, nome: "Mousepad Grid", preco: 49.90, descricao: "Layout em grid, até na área de trabalho." },
],
};
// ─── seu código começa aqui ───────────────────────────────────
Como funciona
- Trabalhem em dupla num único arquivo
vibestore.js - Testem cada tarefa antes de avançar — abram o
index.htmlno navegador com o DevTools aberto (F12) - Não é necessário modificar os arquivos
index.htmlestyles.css
Cronograma
| Aula | Foco | Missões |
|---|---|---|
| 1ª | Menu hamburguer | Missão 1 |
| 2ª | Abas de navegação | Missão 2 |
| 3ª | Modal de detalhes | Missão 3 |
| 4ª | UX avançada + entrega | Missão 4 + envio |
Missão 1 — Menu Hamburguer
Objetivo: fazer o botão ☰ abrir e fechar o menu de navegação.
Tarefa 1.1: selecione o botão #btn-menu e o elemento #menu. Adicione um listener de "click" no botão. No callback, use classList.toggle("ativo") no menu.
Abra o index.html no navegador e clique no botão — o menu deve aparecer e desaparecer a cada clique.
Pergunta 1.1: qual é a diferença entre
classList.add("ativo")eclassList.toggle("ativo")? O que aconteceria comaddse você clicasse duas vezes?
Tarefa 1.2: selecione todos os links do menu com querySelectorAll(".link-menu"). Percorra o resultado com forEach e, para cada link, adicione um listener de "click" que remove a classe "ativo" do menu.
Teste clicando em um link — o menu deve fechar.
Tarefa 1.3: adicione um listener de "keydown" no document. No callback, verifique se event.key === "Escape". Se sim, remova a classe "ativo" do menu.
Teste: abra o menu e pressione Esc — deve fechar.
Pergunta 1.3: por que o listener de
keydownfoi adicionado nodocumente não no botão#btn-menu?
Missão 2 — Abas de Navegação
Objetivo: exibir os produtos da primeira categoria e depois fazer as abas alternar entre elas.
Tarefa 2.1: antes de fazer as abas funcionar, garanta que o conteúdo apareça na tela. Selecione o painel #painel-camisetas. Para cada produto em produtos.camisetas, crie um <div class="card"> com:
- Um
<h3>com o nome do produto - Um
<p>com o preço formatado (ex:R$ 59,90) - Um
<button class="btn-detalhes">comdata-idigual aoiddo produto e texto"Ver detalhes"
Adicione cada card ao painel e recarregue — os cards de camisetas devem aparecer imediatamente.
Dica: use
produto.preco.toFixed(2).replace(".", ",")para formatar o preço.
Tarefa 2.2: selecione todos os botões .btn-aba com querySelectorAll. Para cada botão, adicione um listener de "click". No callback:
- Remova a classe
"ativo"de todos os botões.btn-aba - Adicione
"ativo"apenas no botão clicado (event.target)
Teste clicando nas abas — o estilo ativo deve trocar.
Pergunta 2.2: o que é
event.target? É sempre o mesmo elemento que recebeu oaddEventListener?
Tarefa 2.3: ainda no mesmo listener, adicione a lógica de troca de painel e carregamento:
- Leia o nome da categoria com
event.target.dataset.aba - Selecione todos os
.painele adicione"escondido"em cada um - Remova
"escondido"do painel"#painel-" + aba - Limpe o painel com
innerHTML = ""e popule com os produtos deprodutos[aba](mesmo código da Tarefa 2.1, mas usando a variávelaba)
Teste: clicar nas abas deve trocar o painel e mostrar os produtos da categoria selecionada.
Missão 3 — Modal de Detalhes
Objetivo: abrir o modal com os detalhes do produto ao clicar em “Ver detalhes”.
Tarefa 3.1: selecione o #btn-fechar-modal e o #modal. Adicione um listener de "click" no botão fechar. No callback, adicione a classe "escondido" ao modal.
Teste: abra o DevTools, remova manualmente escondido do modal, depois clique no X — deve fechar.
Tarefa 3.2: em vez de adicionar um listener em cada botão “Ver detalhes” (que são criados dinamicamente), use event delegation. Selecione o elemento <main> e adicione um listener de "click". No callback:
- Verifique se
event.target.classList.contains("btn-detalhes") - Se sim: imprima
event.target.dataset.idno console
Pergunta 3.2: por que usar event delegation em vez de adicionar um listener em cada botão de produto?
Tarefa 3.3: dentro do if da tarefa anterior, use o data-id para encontrar o produto no objeto produtos. Como o objeto tem categorias, você precisará procurar em todas. Depois de encontrar o produto:
- Preencha
#modal-nomecom o nome - Preencha
#modal-descricaocom a descrição - Preencha
#modal-precocom o preço formatado - Remova a classe
"escondido"do modal
Teste: clicar em “Ver detalhes” deve abrir o modal com as informações corretas.
Dica: para buscar o produto em todas as categorias, use
Object.values(produtos).flat().find(p => p.id === id).
Missão 4 — UX Avançada
Objetivo: polir a experiência: bloquear scroll, fechar com Escape e refatorar o código.
Tarefa 4.1: quando o modal abrir, adicione document.body.style.overflow = "hidden" para impedir o scroll da página. Quando fechar, restaure com = "".
Teste com uma página longa (adicione produtos suficientes) — ao abrir o modal, a página não deve rolar.
Tarefa 4.2: reutilize o listener de keydown já criado na Tarefa 1.3. Além de fechar o menu, adicione uma condição para fechar o modal quando Escape for pressionado — mas somente se o modal estiver visível (dica: !modal.classList.contains("escondido")).
Pergunta 4.2: como você verificou se o modal estava aberto antes de agir? Por que essa verificação é necessária?
Tarefa 4.3: extraia duas funções reutilizáveis no início do seu código — antes dos listeners:
function abrirModal(produto) { /* ... */ }
function fecharModal() { /* ... */ }
Refatore todos os pontos que abrem ou fecham o modal para usar essas funções.
Tarefa 4.4 (desafio): implemente o fechamento do modal ao clicar no overlay (fundo escuro). O clique no .modal-conteudo (a caixa branca) não deve fechar o modal.
Dica: adicione um listener de
"click"no#modal. No callback do.modal-conteudo, useevent.stopPropagation()para impedir que o clique suba até o#modal.
Entrega individual
Acesse o Moodle e responda o questionário de reflexão individual — cada membro da dupla entrega o seu próprio.