Javascript

Mini Projeto: Quiz Interativo Já leu

18 min de leitura

Mini Projeto: Quiz Interativo
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&aac

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}°`} &nbsp; ${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

Comentários

Mais em Javascript

Escopo, Hoisting e Closures
Escopo, Hoisting e Closures

Este &eacute; um dos artigos mais importantes da s&eacute;rie. N&atilde;o por...

Objetos: estruturando dados do mundo real
Objetos: estruturando dados do mundo real

Arrays s&atilde;o &oacute;timos para listas. Mas como representar uma&nbsp;pe...

Arrays: criando e manipulando listas
Arrays: criando e manipulando listas

At&eacute; agora trabalhamos com vari&aacute;veis que guardam um &uacute;nico...