diff --git a/assets/css/custom.css b/assets/css/custom.css index 55de913..b20775d 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -49,6 +49,29 @@ a { text-align: center; } +.quiz-progress-wrap { + border: 1px solid var(--border); + border-radius: 12px; + background: #fff7ef; + padding: 0.8rem 0.95rem; +} + +.quiz-progress-bar { + width: 100%; + height: 13px; + border-radius: 999px; + background: #f3dfcc; + overflow: hidden; +} + +.quiz-progress-fill { + height: 100%; + width: 0; + border-radius: inherit; + background: linear-gradient(90deg, #f59e0b, #22c55e); + transition: width 0.35s ease; +} + .question-area { border: 1px solid var(--border); border-radius: 12px; @@ -99,6 +122,11 @@ a { background: #f0fdf4; } +.option-item-correct-pop { + animation: correctPop 0.9s cubic-bezier(0.2, 0.9, 0.28, 1.2); + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.35); +} + .option-item-wrong { border-color: #dc2626; background: #fef2f2; @@ -247,6 +275,15 @@ body.quiz-live #quiz #revealBtn { display: none !important; } +body.quiz-live #quiz .quiz-progress-wrap { + margin-bottom: 1rem !important; + padding: 0.95rem 1.1rem; +} + +body.quiz-live #quiz .quiz-progress-bar { + height: 18px; +} + body.quiz-live #quiz .quiz-actions { display: flex; justify-content: center; @@ -325,3 +362,47 @@ body.lightbox-open { height: 2.4rem; border-radius: 999px; } + +.correct-spark { + position: fixed; + width: var(--size, 14px); + height: var(--size, 14px); + border-radius: 5px; + pointer-events: none; + z-index: 1100; + box-shadow: 0 0 14px rgba(255, 180, 75, 0.75); + transform: translate(-50%, -50%) scale(0.55); + animation: sparkBurst 1.15s ease-out forwards; +} + +@keyframes correctPop { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.42); + } + 35% { + transform: scale(1.1); + box-shadow: 0 0 0 28px rgba(34, 197, 94, 0); + } + 65% { + transform: scale(1.04); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); + } +} + +@keyframes sparkBurst { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.45) rotate(0deg); + } + 18% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.25) rotate(var(--rot)); + } +} diff --git a/assets/js/main.js b/assets/js/main.js index ee27f7b..962ddfe 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -78,14 +78,14 @@ const quizData = [ imageAlt: "Город Витебск" }, { - question: "Белорусский народный инструмент — это…", + question: "Как называется белорусский струнный ударный музыкальный инструмент?", options: ["Цимбалы", "Скрипка", "Дудка", "Сопилка"], answer: 0, - fact: "Цимбалы — струнный инструмент, по ним ударяют молоточками.", + fact: "Цимбалы — это инструмент со струнами, по которым играют маленькими молоточками.", facts: [ - "Цимбалы относятся к струнным ударным инструментам.", - "Музыкант играет на цимбалах двумя маленькими молоточками.", - "Звук цимбал часто можно услышать в белорусской народной музыке." + "Цимбалы похожи на большую деревянную трапецию со струнами.", + "По струнам ударяют двумя маленькими молоточками, и получается звонкий звук.", + "Цимбалы часто звучат в белорусской народной музыке." ], imagePath: "assets/pasted-20260305-190344-ec46e879.jpg", imageAlt: "Традиционный музыкальный инструмент" @@ -100,7 +100,7 @@ const quizData = [ "Замок сочетает элементы готики, ренессанса и барокко.", "Комплекс Мирского замка входит в список Всемирного наследия ЮНЕСКО." ], - imagePath: "assets/images/quiz/q08-castle.jpg", + imagePath: "assets/pasted-20260305-193451-5d1a5abc.jpg", imageAlt: "Мирский замок" }, { @@ -139,7 +139,7 @@ const quizData = [ "В городе находится Софийский собор — важный исторический памятник.", "Полоцк был крупным культурным и торговым центром в Средние века." ], - imagePath: "assets/images/quiz/q11-polotsk.jpg", + imagePath: "assets/pasted-20260305-193618-2a0dd0c5.jpg", imageAlt: "Город Полоцк" }, { @@ -161,6 +161,8 @@ const startBtn = document.getElementById("startQuizBtn"); const quizSection = document.getElementById("quiz"); const summarySection = document.getElementById("summary"); const progressEl = document.getElementById("questionProgress"); +const progressLabel = document.getElementById("progressLabel"); +const progressFill = document.getElementById("progressFill"); const questionText = document.getElementById("questionText"); const optionList = document.getElementById("optionList"); const totalQuestions = document.getElementById("totalQuestions"); @@ -185,37 +187,65 @@ const state = { score: 0, revealed: false, selectedOption: null, - locked: false + locked: false, + lastAnswerCorrect: false, + optionOrders: [] }; totalQuestions.textContent = quizData.length.toString(); +function updateProgressBar() { + const completed = state.index + (state.revealed ? 1 : 0); + const percent = Math.max(0, Math.min(100, Math.round((completed / quizData.length) * 100))); + progressLabel.textContent = `${completed} из ${quizData.length}`; + progressFill.style.width = `${percent}%`; + progressFill.parentElement?.setAttribute("aria-valuenow", String(percent)); +} + +function shuffleArray(items) { + const arr = items.slice(); + for (let i = arr.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +function buildOptionOrders() { + return quizData.map((item) => shuffleArray(item.options.map((_, idx) => idx))); +} + function renderQuestion() { const current = quizData[state.index]; + const currentOrder = state.optionOrders[state.index] || current.options.map((_, idx) => idx); progressEl.textContent = `Вопрос ${state.index + 1} из ${quizData.length}`; + updateProgressBar(); questionText.textContent = current.question; optionList.innerHTML = ""; + optionList.classList.toggle("d-none", state.revealed); + optionList.setAttribute("aria-hidden", state.revealed ? "true" : "false"); - current.options.forEach((option, idx) => { + currentOrder.forEach((originalIdx, displayIdx) => { + const option = current.options[originalIdx]; const li = document.createElement("li"); li.className = "option-item"; li.setAttribute("role", "button"); li.setAttribute("tabindex", "0"); - li.dataset.index = String(idx); + li.dataset.index = String(originalIdx); - if (idx === state.selectedOption) { + if (originalIdx === state.selectedOption) { li.classList.add("option-item-selected"); } if (state.revealed) { - if (idx === current.answer) { + if (originalIdx === current.answer) { li.classList.add("option-item-correct"); - } else if (idx === state.selectedOption) { + } else if (originalIdx === state.selectedOption) { li.classList.add("option-item-wrong"); } } - li.innerHTML = `${String.fromCharCode(65 + idx)}${option}`; + li.innerHTML = `${String.fromCharCode(65 + displayIdx)}${option}`; optionList.appendChild(li); }); @@ -274,6 +304,8 @@ startBtn.addEventListener("click", () => { state.revealed = false; state.selectedOption = null; state.locked = false; + state.lastAnswerCorrect = false; + state.optionOrders = buildOptionOrders(); renderQuestion(); quizSection.scrollIntoView({ behavior: "smooth" }); }); @@ -284,6 +316,7 @@ function goToNextQuestion() { state.revealed = false; state.selectedOption = null; state.locked = false; + state.lastAnswerCorrect = false; renderQuestion(); } else { showSummary(); @@ -297,12 +330,46 @@ restartBtn.addEventListener("click", () => { state.revealed = false; state.selectedOption = null; state.locked = false; + state.lastAnswerCorrect = false; + state.optionOrders = buildOptionOrders(); summarySection.classList.add("d-none"); quizSection.classList.remove("d-none"); renderQuestion(); quizSection.scrollIntoView({ behavior: "smooth" }); }); +function playCorrectClickEffect() { + const selectedEl = optionList.querySelector(`.option-item[data-index="${state.selectedOption}"]`); + if (!selectedEl) return; + + selectedEl.classList.remove("option-item-correct-pop"); + void selectedEl.offsetWidth; + selectedEl.classList.add("option-item-correct-pop"); + + const rect = selectedEl.getBoundingClientRect(); + const burstCount = 28; + const colors = ["#f59e0b", "#f97316", "#fb7185", "#22c55e", "#facc15"]; + + for (let i = 0; i < burstCount; i += 1) { + const spark = document.createElement("span"); + spark.className = "correct-spark"; + spark.style.left = `${rect.left + rect.width / 2}px`; + spark.style.top = `${rect.top + rect.height / 2}px`; + spark.style.background = colors[i % colors.length]; + + const angle = ((Math.PI * 2) / burstCount) * i + (Math.random() * 0.3 - 0.15); + const distance = 88 + Math.random() * 148; + const size = 12 + Math.random() * 18; + spark.style.setProperty("--dx", `${Math.cos(angle) * distance}px`); + spark.style.setProperty("--dy", `${Math.sin(angle) * distance}px`); + spark.style.setProperty("--rot", `${Math.random() * 280 - 140}deg`); + spark.style.setProperty("--size", `${size}px`); + + document.body.appendChild(spark); + setTimeout(() => spark.remove(), 1250); + } +} + function closeLightbox() { imageLightbox.classList.add("d-none"); document.body.classList.remove("lightbox-open"); @@ -334,13 +401,17 @@ function selectOption(index) { if (state.revealed || state.locked) return; const current = quizData[state.index]; state.selectedOption = index; - if (state.selectedOption === current.answer) { + state.lastAnswerCorrect = state.selectedOption === current.answer; + + if (state.lastAnswerCorrect) { + playCorrectClickEffect(); state.score += 1; } state.revealed = true; state.locked = true; renderQuestion(); renderAnswer(); + nextBtn.classList.remove("d-none"); } diff --git a/assets/pasted-20260305-193451-5d1a5abc.jpg b/assets/pasted-20260305-193451-5d1a5abc.jpg new file mode 100644 index 0000000..3ae767f Binary files /dev/null and b/assets/pasted-20260305-193451-5d1a5abc.jpg differ diff --git a/assets/pasted-20260305-193618-2a0dd0c5.jpg b/assets/pasted-20260305-193618-2a0dd0c5.jpg new file mode 100644 index 0000000..47f3dee Binary files /dev/null and b/assets/pasted-20260305-193618-2a0dd0c5.jpg differ diff --git a/assets/vm-shot-2026-03-05T19-56-11-555Z.jpg b/assets/vm-shot-2026-03-05T19-56-11-555Z.jpg new file mode 100644 index 0000000..a609b24 Binary files /dev/null and b/assets/vm-shot-2026-03-05T19-56-11-555Z.jpg differ diff --git a/index.php b/index.php index ef42acf..84e8295 100644 --- a/index.php +++ b/index.php @@ -85,6 +85,15 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; 0 +
+
+ Пройдено + 0 из 12 +
+
+
+
+
Вопрос
@@ -140,7 +149,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
  1. Нажмите «Начать викторину» и выводите вопросы на экран.
  2. Дайте детям 20–30 секунд на обсуждение.
  3. -
  4. Выберите вариант ответа, затем нажмите «Показать ответ».
  5. +
  6. Выберите вариант ответа — карточка с правильным ответом откроется сразу.
  7. В конце покажите итоги и обсудите факты.