Chegamos ao fim do Módulo 2. Em sete artigos você aprendeu a manipular o DOM, reagir a eventos, criar e remover elementos, trabalhar com formulários, persistir dados com LocalStorage e controlar o tempo com temporizadores.
Agora vamos unir tudo isso em um único projeto: um Quiz Interativo completo — com perguntas cronometradas, pontuação, histórico de partidas, feedback visual e persistência de recordes.
Primeiro, a revisão rápida. Depois, o projeto.
Revisão rápida — Módulo 2
DOM — seleção e manipulação
// Selecionando
const titulo = document.querySelector("#titulo");
const itens = document.querySelectorAll(".item");
// Conteúdo
titulo.textContent = "Novo título";
titulo.innerHTML = "Título com <strong>negrito</strong>";
// Classes
titulo.classList.add("destaque");
titulo.classList.toggle("oculto");
titulo.classList.contains("ativo"); // true/false
// Atributos
link.setAttribute("href", "https://...");
link.getAttribute("href");
Criando e removendo elementos
const li = document.createElement("li");
li.textContent = "Novo item";
li.classList.add("item");
lista.appendChild(li);
// Moderno
lista.append(li);
lista.prepend(li);
li.remove();
// Performance com muitos elementos
const fragment = document.createDocumentFragment();
dados.forEach(d => {
const el = document.createElement("div");
el.textContent = d;
fragment.appendChild(el);
});
container.appendChild(fragment);
Eventos
botao.addEventListener("click", (e) => {
e.preventDefault();
console.log(e.target);
});
// Delegação
lista.addEventListener("click", (e) => {
if (e.target.matches("li")) {
e.target.classList.toggle("selecionado");
}
});
// Remover
function handler() { console.log("clicou"); }
botao.addEventListener("click", handler);
botao.removeEventListener("click", handler);
Formulários
form.addEventListener("submit", (e) => {
e.preventDefault();
const dados = Object.fromEntries(new FormData(form).entries());
console.log(dados);
});
campo.addEventListener("blur", () => validarCampo());
campo.addEventListener("input", () => {
if (campo.classList.contains("invalido")) validarCampo();
});
LocalStorage
const storage = {
get: (chave, padrao = null) => {
try {
const item = localStorage.getItem(chave);
return item !== null ? JSON.parse(item) : padrao;
} catch { return padrao; }
},
set: (chave, valor) => {
try {
localStorage.setItem(chave, JSON.stringify(valor));
} catch (e) { console.error(e); }
},
};
Temporizadores
// Uma vez
const id = setTimeout(() => acao(), 2000);
clearTimeout(id);
// Repetidamente
const idInt = setInterval(() => acao(), 1000);
clearInterval(idInt);
// Debounce
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Mini Projeto — Quiz Interativo
O quiz terá:
- 10 perguntas sobre JavaScript
- 20 segundos por pergunta com barra de tempo
- Feedback imediato (certo/errado + explicação)
- Pontuação baseada em velocidade de resposta
- Histórico dos últimos 5 recordes salvo no LocalStorage
- Tela de resultado com estatísticas detalhadas
- Animações e transições suaves
As perguntas
const perguntas = [
{
id: 1,
texto: "Qual palavra-chave declara uma variável que não pode ser reatribuída?",
opcoes: ["var", "let", "const", "static"],
correta: 2,
explicacao: "const declara uma variável cujo vínculo não pode ser reatribuído após a declaração.",
},
{
id: 2,
texto: "Qual método adiciona um elemento ao FINAL de um array?",
opcoes: ["shift()", "unshift()", "pop()", "push()"],
correta: 3,
explicacao: "push() adiciona um ou mais elementos ao final do array e retorna o novo comprimento.",
},
{
id: 3,
texto: "O que o operador === verifica?",
opcoes: [
"Apenas o valor",
"Apenas o tipo",
"Valor e tipo",
"Referência na memória",
],
correta: 2,
explicacao: "=== (igualdade estrita) compara valor E tipo, sem conversão automática.",
},
{
id: 4,
texto: "Qual método do array retorna um NOVO array com elementos transformados?",
opcoes: ["forEach()", "filter()", "map()", "reduce()"],
correta: 2,
explicacao: "map() cria um novo array com os resultados de chamar a função em cada elemento.",
},
{
id: 5,
texto: "O que é uma closure em JavaScript?",
opcoes: [
"Uma função sem parâmetros",
"Uma função que lembra o escopo onde foi criada",
"Um bloco try/catch",
"Um método de array",
],
correta: 1,
explicacao: "Closure é quando uma função retém acesso às variáveis do escopo onde foi definida.",
},
{
id: 6,
texto: "Qual evento do formulário deve sempre ter e.preventDefault()?",
opcoes: ["click", "input", "submit", "blur"],
correta: 2,
explicacao: "submit recarrega a página por padrão. preventDefault() evita esse comportamento.",
},
{
id: 7,
texto: "Como converter um objeto para string JSON?",
opcoes: [
"JSON.parse(obj)",
"JSON.stringify(obj)",
"obj.toString()",
"String(obj)",
],
correta: 1,
explicacao: "JSON.stringify() converte um valor JavaScript em string JSON.",
},
{
id: 8,
texto: "Qual a diferença entre LocalStorage e SessionStorage?",
opcoes: [
"LocalStorage é mais rápido",
"SessionStorage aceita objetos diretamente",
"LocalStorage persiste após fechar o navegador",
"Não há diferença",
],
correta: 2,
explicacao: "LocalStorage persiste indefinidamente. SessionStorage é limpo ao fechar a aba.",
},
{
id: 9,
texto: "O que o método querySelector retorna se não encontrar o elemento?",
opcoes: ["undefined", "false", "null", "0"],
correta: 2,
explicacao: "querySelector retorna null quando nenhum elemento corresponde ao seletor.",
},
{
id: 10,
texto: "Qual padrão cancela e reagenda um setTimeout a cada evento?",
opcoes: ["Throttle", "Debounce", "Polling", "Memoize"],
correta: 1,
explicacao: "Debounce cancela o timer anterior e agenda um novo, executando só após inatividade.",
},
];
O HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Quiz JavaScript</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #232637;
--border: #2e3248;
--accent: #7c6ff7;
--green: #4ade80;
--red: #f87171;
--yellow: #fbbf24;
--text: #e4e6f0;
--muted: #8b8fa8;
}
body {
font-family: 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.tela {
display: none;
width: 100%;
max-width: 640px;
animation: aparecer .3s ease;
}
.tela.ativa { display: block; }
@keyframes aparecer {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Tela Inicial ── */
.inicio {
background: var(--surface);
border-radius: 20px;
padding: 3rem 2rem;
text-align: center;
border: 1px solid var(--border);
}
.emoji { font-size: 4rem; margin-bottom: 1rem; }
.inicio h1 {
font-size: 2rem;
margin-bottom: .5rem;
}
.inicio p {
color: var(--muted);
margin-bottom: 2rem;
line-height: 1.6;
}
.recordes {
background: var(--surface2);
border-radius: 12px;
padding: 1.25rem;
margin-bottom: 2rem;
text-align: left;
}
.recordes h3 {
font-size: .85rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .1em;
margin-bottom: 1rem;
}
.recorde-item {
display: flex;
justify-content: space-between;
padding: .4rem 0;
border-bottom: 1px solid var(--border);
font-size: .9rem;
}
.recorde-item:last-child { border: none; }
.recorde-pontos { color: var(--accent); font-weight: 700; }
/* ── Botões ── */
.btn {
display: inline-block;
padding: .85rem 2rem;
border: none;
border-radius: 10px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: all .2s;
}
.btn-primary {
background: var(--accent);
color: white;
width: 100%;
}
.btn-primary:hover { background: #6c5fe0; transform: translateY(-1px); }
.btn-secondary {
background: var(--surface2);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover { background: var(--border); }
/* ── Tela do Quiz ── */
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.progresso-texto { color: var(--muted); font-size: .9rem; }
.pontuacao-atual { color: var(--accent); font-weight: 700; }
.barra-progresso {
height: 4px;
background: var(--surface2);
border-radius: 999px;
margin-bottom: 1.5rem;
overflow: hidden;
}
.barra-progresso-fill {
height: 100%;
background: var(--accent);
border-radius: 999px;
transition: width .4s ease;
}
/* Timer */
.timer-container {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.timer-numero {
font-size: 1.5rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
min-width: 2rem;
text-align: center;
transition: color .3s;
}
.timer-numero.urgente { color: var(--red); }
.barra-tempo {
flex: 1;
height: 8px;
background: var(--surface2);
border-radius: 999px;
overflow: hidden;
}
.barra-tempo-fill {
height: 100%;
background: var(--accent);
border-radius: 999px;
transition: width .1s linear, background .3s;
}
.barra-tempo-fill.urgente { background: var(--red); }
/* Pergunta */
.card-pergunta {
background: var(--surface);
border-radius: 16px;
padding: 2rem;
margin-bottom: 1rem;
border: 1px solid var(--border);
}
.numero-pergunta {
font-size: .8rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .1em;
margin-bottom: .75rem;
}
.texto-pergunta {
font-size: 1.15rem;
line-height: 1.6;
font-weight: 500;
}
/* Opções */
.opcoes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .75rem;
margin-bottom: 1rem;
}
.opcao {
background: var(--surface);
border: 2px solid var(--border);
border-radius: 12px;
padding: 1rem;
cursor: pointer;
transition: all .2s;
font-size: .95rem;
color: var(--text);
text-align: left;
line-height: 1.4;
}
.opcao:hover:not(:disabled) {
border-color: var(--accent);
background: var(--surface2);
transform: translateY(-1px);
}
.opcao:disabled { cursor: not-allowed; }
.opcao.correta {
border-color: var(--green);
background: rgba(74, 222, 128, .1);
color: var(--green);
}
.opcao.errada {
border-color: var(--red);
background: rgba(248, 113, 113, .1);
color: var(--red);
}
.opcao.neutra {
opacity: .4;
}
/* Explicação */
.explicacao {
background: var(--surface2);
border-radius: 12px;
padding: 1rem 1.25rem;
font-size: .9rem;
color: var(--muted);
margin-bottom: 1rem;
border-left: 3px solid var(--accent);
display: none;
animation: aparecer .3s ease;
}
.explicacao.visivel { display: block; }
.btn-proxima {
width: 100%;
margin-top: .5rem;
display: none;
}
.btn-proxima.visivel { display: block; }
/* ── Tela de Resultado ── */
.resultado {
background: var(--surface);
border-radius: 20px;
padding: 2.5rem 2rem;
text-align: center;
border: 1px solid var(--border);
}
.resultado-emoji { font-size: 4rem; margin-bottom: 1rem; }
.resultado h2 { font-size: 1.75rem; margin-bottom: .5rem; }
.resultado .subtitulo { color: var(--muted); margin-bottom: 2rem; }
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.stat {
background: var(--surface2);
border-radius: 12px;
padding: 1.25rem .75rem;
}
.stat-valor {
font-size: 1.75rem;
font-weight: 700;
color: var(--accent);
display: block;
}
.stat-label {
font-size: .8rem;
color: var(--muted);
margin-top: .25rem;
display: block;
}
.botoes-resultado {
display: flex;
gap: 1rem;
}
.botoes-resultado .btn { flex: 1; }
@media (max-width: 480px) {
.opcoes { grid-template-columns: 1fr; }
.stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- Tela Inicial -->
<div class="tela ativa" id="tela-inicio">
<div class="inicio">
<div class="emoji">🧠</div>
<h1>Quiz JavaScript</h1>
<p>10 perguntas · 20 segundos cada · Quanto mais rápido, mais pontos!</p>
<div class="recordes" id="recordes">
<h3>🏆 Melhores pontuações</h3>
<div id="lista-recordes"></div>
</div>
<button class="btn btn-primary" id="btn-iniciar">Começar Quiz</button>
</div>
</div>
<!-- Tela do Quiz -->
<div class="tela" id="tela-quiz">
<div class="quiz-header">
<span class="progresso-texto" id="progresso-texto">Pergunta 1 de 10</span>
<span class="pontuacao-atual" id="pontuacao-atual">0 pts</span>
</div>
<div class="barra-progresso">
<div class="barra-progresso-fill" id="barra-progresso-fill"></div>
</div>
<div class="timer-container">
<span class="timer-numero" id="timer-numero">20</span>
<div class="barra-tempo">
<div class="barra-tempo-fill" id="barra-tempo-fill"></div>
</div>
</div>
<div class="card-pergunta">
<div class="numero-pergunta" id="numero-pergunta"></div>
<div class="texto-pergunta" id="texto-pergunta"></div>
</div>
<div class="opcoes" id="opcoes"></div>
<div class="explicacao" id="explicacao"></div>
<button class="btn btn-primary btn-proxima" id="btn-proxima">
Próxima pergunta →
</button>
</div>
<!-- Tela de Resultado -->
<div class="tela" id="tela-resultado">
<div class="resultado">
<div class="resultado-emoji" id="resultado-emoji"></div>
<h2 id="resultado-titulo"></h2>
<p class="subtitulo" id="resultado-subtitulo"></p>
<div class="stats">
<div class="stat">
<span class="stat-valor" id="stat-pontos"></span>
<span class="stat-label">Pontuação</span>
</div>
<div class="stat">
<span class="stat-valor" id="stat-acertos"></span>
<span class="stat-label">Acertos</span>
</div>
<div class="stat">
<span class="stat-valor" id="stat-tempo"></span>
<span class="stat-label">Tempo médio</span>
</div>
</div>
<div class="botoes-resultado">
<button class="btn btn-secondary" id="btn-menu">Menu</button>
<button class="btn btn-primary" id="btn-jogar-novamente">Jogar novamente</button>
</div>
</div>
</div>
<script>
// ──────────────────────────────────────────────────────
// DADOS
// ──────────────────────────────────────────────────────
const perguntas = [
{
id: 1,
texto: "Qual palavra-chave declara uma variável que não pode ser reatribuída?",
opcoes: ["var", "let", "const", "static"],
correta: 2,
explicacao: "const declara uma variável cujo vínculo não pode ser reatribuído após a declaração.",
},
{
id: 2,
texto: "Qual método adiciona um elemento ao FINAL de um array?",
opcoes: ["shift()", "unshift()", "pop()", "push()"],
correta: 3,
explicacao: "push() adiciona um ou mais elementos ao final do array e retorna o novo comprimento.",
},
{
id: 3,
texto: "O que o operador === verifica?",
opcoes: ["Apenas o valor", "Apenas o tipo", "Valor e tipo", "Referência na memória"],
correta: 2,
explicacao: "=== (igualdade estrita) compara valor E tipo, sem conversão automática.",
},
{
id: 4,
texto: "Qual método do array retorna um NOVO array com elementos transformados?",
opcoes: ["forEach()", "filter()", "map()", "reduce()"],
correta: 2,
explicacao: "map() cria um novo array com os resultados de chamar a função em cada elemento.",
},
{
id: 5,
texto: "O que é uma closure em JavaScript?",
opcoes: [
"Uma função sem parâmetros",
"Uma função que lembra o escopo onde foi criada",
"Um bloco try/catch",
"Um método de array",
],
correta: 1,
explicacao: "Closure é quando uma função retém acesso às variáveis do escopo onde foi definida.",
},
{
id: 6,
texto: "Qual evento do formulário deve sempre ter e.preventDefault()?",
opcoes: ["click", "input", "submit", "blur"],
correta: 2,
explicacao: "submit recarrega a página por padrão. preventDefault() evita esse comportamento.",
},
{
id: 7,
texto: "Como converter um objeto para string JSON?",
opcoes: ["JSON.parse(obj)", "JSON.stringify(obj)", "obj.toString()", "String(obj)"],
correta: 1,
explicacao: "JSON.stringify() converte um valor JavaScript em string JSON.",
},
{
id: 8,
texto: "Qual a diferença entre LocalStorage e SessionStorage?",
opcoes: [
"LocalStorage é mais rápido",
"SessionStorage aceita objetos diretamente",
"LocalStorage persiste após fechar o navegador",
"Não há diferença",
],
correta: 2,
explicacao: "LocalStorage persiste indefinidamente. SessionStorage é limpo ao fechar a aba.",
},
{
id: 9,
texto: "O que querySelector retorna se não encontrar o elemento?",
opcoes: ["undefined", "false", "null", "0"],
correta: 2,
explicacao: "querySelector retorna null quando nenhum elemento corresponde ao seletor.",
},
{
id: 10,
texto: "Qual padrão cancela e reagenda um setTimeout a cada evento?",
opcoes: ["Throttle", "Debounce", "Polling", "Memoize"],
correta: 1,
explicacao: "Debounce cancela o timer anterior e agenda um novo, executando só após inatividade.",
},
];
// ──────────────────────────────────────────────────────
// STORAGE
// ──────────────────────────────────────────────────────
const storage = {
get: (chave, padrao = null) => {
try {
const item = localStorage.getItem(chave);
return item !== null ? JSON.parse(item) : padrao;
} catch { return padrao; }
},
set: (chave, valor) => {
try { localStorage.setItem(chave, JSON.stringify(valor)); }
catch (e) { console.error(e); }
},
};
// ──────────────────────────────────────────────────────
// ESTADO DO JOGO
// ──────────────────────────────────────────────────────
let estado = {
perguntaAtual: 0,
pontuacao: 0,
acertos: 0,
temposResposta: [],
inicioPergunta: null,
timerIntervalo: null,
tempoRestante: 20,
respondeu: false,
perguntasEmbaralhadas: [],
};
// ──────────────────────────────────────────────────────
// REFERÊNCIAS
// ──────────────────────────────────────────────────────
const telas = {
inicio: document.querySelector("#tela-inicio"),
quiz: document.querySelector("#tela-quiz"),
resultado: document.querySelector("#tela-resultado"),
};
const el = {
listaRecordes: document.querySelector("#lista-recordes"),
progressoTexto: document.querySelector("#progresso-texto"),
pontuacaoAtual: document.querySelector("#pontuacao-atual"),
barraProgressoFill: document.querySelector("#barra-progresso-fill"),
timerNumero: document.querySelector("#timer-numero"),
barraTempoFill: document.querySelector("#barra-tempo-fill"),
numeroPergunta: document.querySelector("#numero-pergunta"),
textoPergunta: document.querySelector("#texto-pergunta"),
opcoes: document.querySelector("#opcoes"),
explicacao: document.querySelector("#explicacao"),
btnProxima: document.querySelector("#btn-proxima"),
resultadoEmoji: document.querySelector("#resultado-emoji"),
resultadoTitulo: document.querySelector("#resultado-titulo"),
resultadoSubtitulo: document.querySelector("#resultado-subtitulo"),
statPontos: document.querySelector("#stat-pontos"),
statAcertos: document.querySelector("#stat-acertos"),
statTempo: document.querySelector("#stat-tempo"),
};
// ──────────────────────────────────────────────────────
// UTILITÁRIOS
// ──────────────────────────────────────────────────────
function mostrarTela(nome) {
Object.values(telas).forEach(t => t.classList.remove("ativa"));
telas[nome].classList.add("ativa");
}
function embaralhar(array) {
return [...array].sort(() => Math.random() - 0.5);
}
function calcularPontos(tempoRestante, acertou) {
if (!acertou) return 0;
// Pontuação base + bônus de velocidade
return 100 + Math.floor(tempoRestante * 10);
}
// ──────────────────────────────────────────────────────
// RECORDES
// ──────────────────────────────────────────────────────
function carregarRecordes() {
const recordes = storage.get("quiz-recordes", []);
if (recordes.length === 0) {
el.listaRecordes.innerHTML =
'<p style="color: var(--muted); font-size: .9rem;">Nenhuma partida ainda. Seja o primeiro!</p>';
return;
}
el.listaRecordes.innerHTML = "";
recordes.forEach((r, i) => {
const div = document.createElement("div");
div.classList.add("recorde-item");
const medalhas = ["🥇", "🥈", "🥉", "4°", "5°"];
div.innerHTML = `
<span>${medalhas[i] || `${i + 1}°`} ${r.acertos}/10 acertos · ${r.data}</span>
<span class="recorde-pontos">${r.pontuacao} pts</span>
`;
el.listaRecordes.appendChild(div);
});
}
function salvarRecorde(pontuacao, acertos) {
const recordes = storage.get("quiz-recordes", []);
recordes.push({
pontuacao,
acertos,
data: new Date().toLocaleDateString("pt-BR"),
});
// Ordena por pontuação e mantém os 5 melhores
recordes.sort((a, b) => b.pontuacao - a.pontuacao);
storage.set("quiz-recordes", recordes.slice(0, 5));
}
// ──────────────────────────────────────────────────────
// TIMER
// ──────────────────────────────────────────────────────
function iniciarTimer() {
estado.tempoRestante = 20;
estado.inicioPergunta = Date.now();
atualizarTimer();
estado.timerIntervalo = setInterval(() => {
estado.tempoRestante -= 0.1;
if (estado.tempoRestante <= 0) {
estado.tempoRestante = 0;
atualizarTimer();
clearInterval(estado.timerIntervalo);
if (!estado.respondeu) timeoutPergunta();
} else {
atualizarTimer();
}
}, 100);
}
function atualizarTimer() {
const segundos = Math.ceil(estado.tempoRestante);
const porcentagem = (estado.tempoRestante / 20) * 100;
const urgente = estado.tempoRestante <= 5;
el.timerNumero.textContent = segundos;
el.timerNumero.classList.toggle("urgente", urgente);
el.barraTempoFill.style.width = `${porcentagem}%`;
el.barraTempoFill.classList.toggle("urgente", urgente);
}
function pararTimer() {
clearInterval(estado.timerIntervalo);
}
// ──────────────────────────────────────────────────────
// QUIZ
// ──────────────────────────────────────────────────────
function iniciarQuiz() {
estado = {
...estado,
perguntaAtual: 0,
pontuacao: 0,
acertos: 0,
temposResposta: [],
respondeu: false,
perguntasEmbaralhadas: embaralhar(perguntas),
};
mostrarTela("quiz");
carregarPergunta();
}
function carregarPergunta() {
const pergunta = estado.perguntasEmbaralhadas[estado.perguntaAtual];
const total = estado.perguntasEmbaralhadas.length;
const numero = estado.perguntaAtual + 1;
// Header
el.progressoTexto.textContent = `Pergunta ${numero} de ${total}`;
el.pontuacaoAtual.textContent = `${estado.pontuacao} pts`;
el.barraProgressoFill.style.width = `${((numero - 1) / total) * 100}%`;
// Pergunta
el.numeroPergunta.textContent = `Pergunta ${numero}`;
el.textoPergunta.textContent = pergunta.texto;
// Opções
el.opcoes.innerHTML = "";
pergunta.opcoes.forEach((opcao, index) => {
const btn = document.createElement("button");
btn.classList.add("opcao");
btn.textContent = opcao;
btn.addEventListener("click", () => responder(index));
el.opcoes.appendChild(btn);
});
// Limpa feedback anterior
el.explicacao.textContent = "";
el.explicacao.classList.remove("visivel");
el.btnProxima.classList.remove("visivel");
estado.respondeu = false;
iniciarTimer();
}
function responder(indiceEscolhido) {
if (estado.respondeu) return;
estado.respondeu = true;
pararTimer();
const pergunta = estado.perguntasEmbaralhadas[estado.perguntaAtual];
const acertou = indiceEscolhido === pergunta.correta;
const tempoGasto = 20 - estado.tempoRestante;
estado.temposResposta.push(tempoGasto);
if (acertou) {
estado.acertos++;
const pts = calcularPontos(estado.tempoRestante, true);
estado.pontuacao += pts;
}
// Feedback visual nas opções
const botoes = el.opcoes.querySelectorAll(".opcao");
botoes.forEach((btn, i) => {
btn.disabled = true;
if (i === pergunta.correta) {
btn.classList.add("correta");
} else if (i === indiceEscolhido && !acertou) {
btn.classList.add("errada");
} else {
btn.classList.add("neutra");
}
});
// Explicação
el.explicacao.textContent = `💡 ${pergunta.explicacao}`;
el.explicacao.classList.add("visivel");
// Botão próxima
const ultima = estado.perguntaAtual === estado.perguntasEmbaralhadas.length - 1;
el.btnProxima.textContent = ultima ? "Ver resultado →" : "Próxima pergunta →";
el.btnProxima.classList.add("visivel");
// Atualiza pontuação no header
el.pontuacaoAtual.textContent = `${estado.pontuacao} pts`;
}
function timeoutPergunta() {
if (estado.respondeu) return;
estado.respondeu = true;
const pergunta = estado.perguntasEmbaralhadas[estado.perguntaAtual];
estado.temposResposta.push(20);
const botoes = el.opcoes.querySelectorAll(".opcao");
botoes.forEach((btn, i) => {
btn.disabled = true;
if (i === pergunta.correta) btn.classList.add("correta");
else btn.classList.add("neutra");
});
el.explicacao.textContent = `⏰ Tempo esgotado! ${pergunta.explicacao}`;
el.explicacao.classList.add("visivel");
const ultima = estado.perguntaAtual === estado.perguntasEmbaralhadas.length - 1;
el.btnProxima.textContent = ultima ? "Ver resultado →" : "Próxima pergunta →";
el.btnProxima.classList.add("visivel");
}
function proximaPergunta() {
estado.perguntaAtual++;
if (estado.perguntaAtual >= estado.perguntasEmbaralhadas.length) {
encerrarQuiz();
} else {
carregarPergunta();
}
}
function encerrarQuiz() {
pararTimer();
salvarRecorde(estado.pontuacao, estado.acertos);
const tempoMedio = estado.temposResposta.length > 0
? (estado.temposResposta.reduce((a, b) => a + b, 0) / estado.temposResposta.length).toFixed(1)
: 0;
const porcentagem = Math.round((estado.acertos / perguntas.length) * 100);
// Emoji e título baseados no desempenho
const resultados = [
{ min: 90, emoji: "🏆", titulo: "Incrível!", subtitulo: "Você é um mestre do JavaScript!" },
{ min: 70, emoji: "🎉", titulo: "Muito bom!", subtitulo: "Você domina bem o JavaScript!" },
{ min: 50, emoji: "👍", titulo: "Bom trabalho!", subtitulo: "Continue praticando!" },
{ min: 0, emoji: "📚", titulo: "Continue estudando!", subtitulo: "Revise os artigos e tente novamente." },
];
const resultado = resultados.find(r => porcentagem >= r.min);
el.resultadoEmoji.textContent = resultado.emoji;
el.resultadoTitulo.textContent = resultado.titulo;
el.resultadoSubtitulo.textContent = resultado.subtitulo;
el.statPontos.textContent = estado.pontuacao;
el.statAcertos.textContent = `${estado.acertos}/10`;
el.statTempo.textContent = `${tempoMedio}s`;
mostrarTela("resultado");
carregarRecordes();
}
// ──────────────────────────────────────────────────────
// EVENTOS
// ──────────────────────────────────────────────────────
document.querySelector("#btn-iniciar").addEventListener("click", iniciarQuiz);
document.querySelector("#btn-jogar-novamente").addEventListener("click", iniciarQuiz);
document.querySelector("#btn-menu").addEventListener("click", () => mostrarTela("inicio"));
el.btnProxima.addEventListener("click", proximaPergunta);
// Atalho de teclado: 1, 2, 3, 4 para responder
document.addEventListener("keydown", (e) => {
if (telas.quiz.classList.contains("ativa") && !estado.respondeu) {
const mapa = { "1": 0, "2": 1, "3": 2, "4": 3 };
if (mapa[e.key] !== undefined) responder(mapa[e.key]);
}
if (e.key === "Enter" && el.btnProxima.classList.contains("visivel")) {
proximaPergunta();
}
});
// ──────────────────────────────────────────────────────
// INICIALIZAÇÃO
// ──────────────────────────────────────────────────────
carregarRecordes();
</script>
</body>
</html>
O que este projeto exercitou
| Conceito do Módulo 2 | Onde foi usado |
|---|---|
| Seleção de elementos | Todas as referências do quiz |
| classList e toggle | Feedback visual das opções e timer |
| createElement e append | Botões de opção gerados dinamicamente |
| DocumentFragment | Renderização da lista de recordes |
| Eventos de clique | Opções, botões e teclas |
| Delegação de eventos | Atalhos de teclado globais |
| Formulários / e.preventDefault | Lógica de submissão do quiz |
| LocalStorage | Persistência dos 5 melhores recordes |
| setInterval | Timer de 20 segundos por pergunta |
| clearInterval | Parar timer ao responder ou esgotar |
| Debounce (conceito) | Proteção contra duplo clique (respondeu) |
| Animações CSS via JS | Transições de tela e entrada de elementos |
📚 Fontes e Referências
- MDN Web Docs — DOM: https://developer.mozilla.org/pt-BR/docs/Web/API/Document_Object_Model
- MDN Web Docs — Web Storage API: https://developer.mozilla.org/pt-BR/docs/Web/API/Web_Storage_API
- MDN Web Docs — setInterval: https://developer.mozilla.org/pt-BR/docs/Web/API/setInterval
- JavaScript.info — Document, Events, Interfaces: https://javascript.info/document
- Eloquent JavaScript, Cap. 13-15: https://eloquentjavascript.net
- JavaScript & JQuery — Jon Duckett (Alta Books)
- web.dev — Learn JavaScript: https://web.dev/learn/javascript
- freeCodeCamp — JavaScript DOM Manipulation: https://www.freecodecamp.org/news/javascript-dom-manipulation