Autosave: 20260305-195720

This commit is contained in:
Flatlogic Bot 2026-03-05 19:57:20 +00:00
parent 4227c05243
commit aae6b26656
6 changed files with 177 additions and 16 deletions

View File

@ -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));
}
}

View File

@ -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 = `<span class="option-index">${String.fromCharCode(65 + idx)}</span><span>${option}</span>`;
li.innerHTML = `<span class="option-index">${String.fromCharCode(65 + displayIdx)}</span><span>${option}</span>`;
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");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -85,6 +85,15 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<span class="fs-4 fw-semibold" id="classScore">0</span>
</div>
</div>
<div class="quiz-progress-wrap mb-3" aria-live="polite">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<span class="small text-muted">Пройдено</span>
<span class="small fw-semibold" id="progressLabel">0 из 12</span>
</div>
<div class="quiz-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Прогресс по вопросам">
<div class="quiz-progress-fill" id="progressFill"></div>
</div>
</div>
<div class="question-area">
<div class="question-label text-uppercase small text-muted mb-2">Вопрос</div>
@ -140,7 +149,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<ol class="text-muted">
<li>Нажмите «Начать викторину» и выводите вопросы на экран.</li>
<li>Дайте детям 2030 секунд на обсуждение.</li>
<li>Выберите вариант ответа, затем нажмите «Показать ответ».</li>
<li>Выберите вариант ответа карточка с правильным ответом откроется сразу.</li>
<li>В конце покажите итоги и обсудите факты.</li>
</ol>
</article>