PageSourceSearch

https://itlearn360.com/dist/js/views/quizPublic.js?v=20260919-v376

js itlearn360.com collected 2026-10-10 00:09:33 UTC 21,456 bytes, 450 lines download raw bytes

1import { getJSON, getURL } from "../api.js";
2import { navigate } from "../router.js";
3import { renderAdsense } from "../ui/adsense.js?v=20260917-v352";
4import { applySeo } from "../ui/seo.js";
5import { getQuizAttempt, saveQuizAttempt } from "../state.js";
6import { getPracticePackage, getQuizPracticeMetadata } from "../data/practicePackages.js?v=20260919-v375";
7
8function esc(s) {
9  return String(s ?? "")
10    .replaceAll("&", "&")
11    .replaceAll("<", "&lt;")
12    .replaceAll(">", "&gt;")
13    .replaceAll('"', "&quot;")
14    .replaceAll("'", "&#039;");
15}
16
17function icon(name) {
18  const body = {
19    quiz: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h.01M11 8h5M8 12h.01M11 12h5M8 16h.01M11 16h3"/>',
20    course: '<path d="M5 4.5h12.5A1.5 1.5 0 0 1 19 6v13H6a2 2 0 0 1-2-2V5.5a1 1 0 0 1 1-1Z"/><path d="M6 16h13M8 8h7"/>',
21    pack: '<rect x="3" y="5" width="18" height="14" rx="3"/><path d="M7 9h10M7 13h7"/>',
22    clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
23    repeat: '<path d="M20 7h-9a5 5 0 0 0 0 10h1"/><path d="m17 4 3 3-3 3M4 17h9a5 5 0 0 0 0-10h-1"/><path d="m7 20-3-3 3-3"/>',
24    check: '<path d="m5 12 4 4L19 6"/>',
25    arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>',
26  }[name] || "";
27  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
28}
29
30function flattenPackageAssessments(pkg) {
31  if (!pkg?.sections?.length) return [];
32  return pkg.sections.flatMap((section, sectionIndex) =>
33    (section.assessments || []).map((assessment, assessmentIndex) => ({
34      ...assessment,
35      sectionTitle: section.title,
36      sectionOrder: sectionIndex + 1,
37      assessmentOrderInSection: assessmentIndex + 1,
38    }))
39  );
40}
41
42function quizHref(assessment) {
43  return `/q/${assessment.courseId}/${assessment.moduleId}/${assessment.quizId}`;
44}
45
46export async function renderPublicQuiz(root, courseId, moduleId, quizId) {
47  const catalog = await getJSON("/content/catalog.json");
48  const courseItem = (catalog.courses || []).find((c) => c.id === courseId);
49  if (!courseItem) throw new Error("Course not found");
50
51  const course = await getJSON(courseItem.path);
52  const modRef = (course.modules || []).find((m) => m.id === moduleId);
53  if (!modRef) throw new Error("Module not found");
54
55  const mod = await getJSON(modRef.path);
56  const quizMeta = (mod.quizzes || []).find((q) => q.id === quizId);
57  if (!quizMeta) throw new Error("Quiz not found");
58
59  const practiceMeta = getQuizPracticeMetadata(courseId, moduleId, quizId);
60  const primaryMembership = practiceMeta?.packageMemberships?.[0] || (practiceMeta ? {
61    packageId: practiceMeta.packageId,
62    packageSlug: practiceMeta.packageSlug,
63    packageTitle: practiceMeta.packageTitle,
64    sectionTitle: practiceMeta.sectionTitle,
65  } : null);
66  const primaryPackage = primaryMembership?.packageSlug ? getPracticePackage(primaryMembership.packageSlug) : null;
67  const packageAssessments = flattenPackageAssessments(primaryPackage);
68  const currentDocId = `${courseId}__${moduleId}__${quizId}`;
69  const currentPackageIndex = packageAssessments.findIndex((assessment) => assessment.docId === currentDocId);
70  const currentPackagePosition = currentPackageIndex >= 0 ? currentPackageIndex + 1 : null;
71  const nextPackageAssessment = currentPackageIndex >= 0 && currentPackageIndex + 1 < packageAssessments.length
72    ? packageAssessments[currentPackageIndex + 1]
73    : null;
74  const packagePassedCount = packageAssessments.filter((assessment) => getQuizAttempt(assessment.courseId, assessment.moduleId, assessment.quizId)?.pass).length;
75  const packageProgressPercent = packageAssessments.length ? Math.round((packagePassedCount / packageAssessments.length) * 100) : 0;
76  const packageDisplayTitle = primaryPackage?.shortTitle || primaryMembership?.packageTitle || practiceMeta?.packageTitle || "";
77  const packageUrl = primaryMembership?.packageSlug ? `/skill-assessment/${primaryMembership.packageSlug}` : "";
78  const quizTitle = practiceMeta?.displayTitle || quizMeta.title || quizMeta.id;
79  const questions = await getJSON(quizMeta.path);
80  const total = Array.isArray(questions) ? questions.length : 0;
81  const moduleTitle = modRef.title || moduleId;
82  const canonicalPath = `/q/${courseId}/${moduleId}/${quizId}`;
83  const description = `Free practice quiz: ${quizTitle}. Course: ${course.title}. Module: ${moduleTitle}. ${total} questions. Instant score report.`;
84  const image = course.cover
85    ? String(course.cover).startsWith("http")
86      ? String(course.cover)
87      : getURL(course.cover)
88    : getURL(`/content/images/${courseId}.jpg`);
89
90  applySeo({
91    id: `quiz:${courseId}:${moduleId}:${quizId}`,
92    title: `${quizTitle} | Free Practice Quiz | ${course.title}`,
93    description,
94    canonicalPath,
95    image,
96    type: "website",
97    noindex: true,
98  });
99
100  let idx = 0;
101  const answers = {};
102  let submitted = false;
103
104  const courseCategory = course.categoryId || course.category || "";
105  const relatedCourses = (catalog.courses || [])
106    .filter((c) => c.id !== courseId && (c.categoryId === courseCategory || c.category === courseCategory))
107    .slice(0, 2);
108
109  const relatedCoursesHTML = relatedCourses.length
110    ? `
111      <section class="quizRelatedCoursesV335">
112        <div class="quizAsideEyebrowV335">RELATED COURSES</div>
113        <div class="quizRelatedCourseListV335">
114          ${relatedCourses
115            .map(
116              (c) => `
117                <button type="button" class="quizRelatedCourseV335" data-nav="/course/${esc(c.id)}">
118                  <span class="quizRelatedCourseImageV335">
119                    <img src="${esc(getURL(`/content/images/${c.id}.jpg`))}" alt="" onerror="this.style.display='none'" />
120                  </span>
121                  <span>
122                    <strong>${esc(c.title)}</strong>
123                    <small>Explore course ${icon("arrow")}</small>
124                  </span>
125                </button>
126              `
127            )
128            .join("")}
129        </div>
130      </section>
131    `
132    : "";
133
134  const packageRailHTML = primaryPackage && packageUrl
135    ? `
136      <section class="quizPackageRailV369">
137        <div class="quizAsideEyebrowV335">YOUR SKILL ASSESSMENT PACK</div>
138        <a class="quizPackageRailTitleV369" href="${esc(packageUrl)}" data-link>
139          <span>${icon("pack")}</span>
140          <strong>${esc(packageDisplayTitle)}</strong>
141          ${icon("arrow")}
142        </a>
143        <p class="quizPackageRailContextV369">${currentPackagePosition ? `Assessment ${currentPackagePosition} of ${packageAssessments.length}` : `${packageAssessments.length} assessments`}${primaryMembership?.sectionTitle ? ` • ${esc(primaryMembership.sectionTitle)}` : ""}</p>
144        <div class="quizPackageProgressV369" data-package-progress-track aria-label="${packageProgressPercent}% of assessment pack completed"><i data-package-progress-bar style="width:${packageProgressPercent}%"></i></div>
145        <div class="quizPackageProgressMetaV369"><span><strong data-package-passed-count>${packagePassedCount}</strong> of ${packageAssessments.length} passed</span><strong data-package-progress-percent>${packageProgressPercent}%</strong></div>
146        <button type="button" class="quizBtnV335 primary quizPackageOpenBtnV369" data-open-package>View full skill assessment ${icon("arrow")}</button>
147      </section>
148    `
149    : "";
150
151  root.innerHTML = `
152    <div class="quizPublicV335">
153      <section class="quizPublicHeroV335">
154        <div class="quizPublicHeroInnerV335">
155          <div class="quizPublicHeroCopyV335">
156            ${packageUrl ? `<nav class="quizPackageBreadcrumbV369" aria-label="Breadcrumb"><a href="/skill-assessment" data-link>Skill Assessments</a><span>›</span><a href="${esc(packageUrl)}" data-link>${esc(packageDisplayTitle)}</a><span>›</span><span>Current assessment</span></nav>` : ""}
157            <div class="quizEyebrowV335">${practiceMeta ? esc(practiceMeta.contentType || "SKILL ASSESSMENT") : "FREE PRACTICE QUIZ"}</div>
158            <h1>${esc(quizTitle)}</h1>
159            ${packageUrl ? `<a class="quizPackageConnectionV369" href="${esc(packageUrl)}" data-link><span class="quizPackageConnectionIconV369">${icon("pack")}</span><span><small>Included in Skill Assessment Pack</small><strong>${esc(packageDisplayTitle)}</strong></span>${icon("arrow")}</a>` : ""}
160            ${practiceMeta?.packageMemberships?.length > 1 ? `<div class="quizPackageLinksV356">${practiceMeta.packageMemberships.slice(1).map((membership) => `<a class="quizPackageLinkV353" href="/skill-assessment/${esc(membership.packageSlug)}" data-link>Also included in ${esc(membership.packageTitle)}</a>`).join("")}</div>` : ""}
161            <p>${esc(course.title)} <span>•</span> ${esc(moduleTitle)}</p>
162            <div class="quizPublicFactsV335">
163              <span>${icon("quiz")} ${total} questions</span>
164              <span>${icon("check")} Instant results</span>
165              <span>${icon("repeat")} Retake anytime</span>
166            </div>
167          </div>
168          <div class="quizPublicHeroActionsV335">
169            <button id="quizJumpStartBtn" class="quizBtnV335 primary" type="button">Start assessment ${icon("arrow")}</button>
170            ${packageUrl ? `<button id="openPackageBtn" class="quizBtnV335 secondary" type="button">${icon("pack")} View skill assessment pack</button>` : ""}
171            <button id="openCourseBtn" class="quizBtnV335 secondary" type="button">${icon("course")} View course</button>
172          </div>
173        </div>
174      </section>
175
176      <section class="quizPublicBodyV335">
177        <div class="quizPublicLayoutV335" id="pubQuizGrid">
178          <main class="quizPublicMainV335">
179            <section class="quizQuestionCardV335 quizPublicQuestionCardV335">
180              <div class="quizQuestionCountV335">Question <span id="qNum"></span> of ${total}</div>
181              <h2 id="qText" class="quizQuestionTextV335"></h2>
182              <div id="opts" class="quizOptionsV335"></div>
183
184              <div class="quizQuestionNavV335">
185                <button id="prevBtn" class="quizBtnV335 secondary" type="button">← Previous</button>
186                <button id="nextBtn" class="quizBtnV335 secondary" type="button">Next →</button>
187              </div>
188
189              <div class="quizSubmitStackV335">
190                <button id="submitBtn" class="quizBtnV335 primary" type="button">Submit quiz</button>
191                <button id="retryBtn" class="quizBtnV335 secondary" type="button" style="display:none">Retake quiz</button>
192              </div>
193
194              <div id="resultBox" class="quizResultBoxV335" style="display:none"></div>
195              <div id="reportBox" class="quizReportBoxV335" style="display:none"></div>
196            </section>
197          </main>
198
199          <aside class="quizPublicAsideV335">
200            ${packageRailHTML}
201            <section class="quizAdCardV335">
202              <div class="quizAsideEyebrowV335">ADVERTISEMENT</div>
203              <div id="adSide1"></div>
204            </section>
205            ${relatedCoursesHTML}
206          </aside>
207        </div>
208      </section>
209    </div>
210  `;
211
212  root.querySelector("#openCourseBtn").onclick = () => navigate(`/course/${courseId}`);
213  root.querySelector("#openPackageBtn")?.addEventListener("click", () => { if (packageUrl) navigate(packageUrl); });
214  root.querySelector("[data-open-package]")?.addEventListener("click", () => { if (packageUrl) navigate(packageUrl); });
215  const jumpBtn = root.querySelector("#quizJumpStartBtn");
216  if (jumpBtn) jumpBtn.onclick = () => root.querySelector("#pubQuizGrid")?.scrollIntoView({ behavior: "smooth", block: "start" });
217
218  renderAdsense(root.querySelector("#adSide1"), { slot: "5606596534", width: 300, height: 250, minHeight: 250 });
219
220  root.querySelectorAll("[data-nav]").forEach((el) => {
221    el.addEventListener("click", () => navigate(el.getAttribute("data-nav")));
222  });
223
224  const qNumEl = root.querySelector("#qNum");
225  const qTextEl = root.querySelector("#qText");
226  const optsEl = root.querySelector("#opts");
227  const resultBox = root.querySelector("#resultBox");
228  const reportBox = root.querySelector("#reportBox");
229  const prevBtn = root.querySelector("#prevBtn");
230  const nextBtn = root.querySelector("#nextBtn");
231  const submitBtn = root.querySelector("#submitBtn");
232  const retryBtn = root.querySelector("#retryBtn");
233
234  function lockInputs() {
235    optsEl.querySelectorAll("input").forEach((inp) => (inp.disabled = true));
236    optsEl.querySelectorAll(".q-opt-label").forEach((label) => (label.style.pointerEvents = "none"));
237  }
238
239  function unlockInputs() {
240    optsEl.querySelectorAll("input").forEach((inp) => (inp.disabled = false));
241    optsEl.querySelectorAll(".q-opt-label").forEach((label) => (label.style.pointerEvents = "auto"));
242  }
243
244  function renderQuestion() {
245    if (!Array.isArray(questions) || !questions.length) return;
246    const q = questions[idx];
247    qNumEl.textContent = String(idx + 1);
248    qTextEl.textContent = String(q.question || "");
249    const correctIdx = Number(q.correctAnswer);
250
251    optsEl.innerHTML = (q.options || [])
252      .map((opt, i) => {
253        const picked = answers[idx];
254        const isPicked = picked === i;
255        const isCorrect = i === correctIdx;
256        let stateClass = "";
257        let badge = "";
258        if (submitted) {
259          if (isCorrect) {
260            stateClass = "is-correct";
261            badge = `<span class="quizAnswerBadgeV335 correct">Correct</span>`;
262          } else if (isPicked) {
263            stateClass = "is-wrong";
264            badge = `<span class="quizAnswerBadgeV335 wrong">Your answer</span>`;
265          }
266        }
267
268        return `
269          <label class="q-opt-label ${stateClass}">
270            <input type="radio" name="opt" value="${i}" ${isPicked ? "checked" : ""} ${submitted ? "disabled" : ""} />
271            <span class="quizOptionTextV335">${esc(opt)}</span>
272            ${badge}
273          </label>
274        `;
275      })
276      .join("");
277
278    if (!submitted) {
279      optsEl.querySelectorAll("input").forEach((inp) => {
280        inp.onchange = () => {
281          answers[idx] = Number(inp.value);
282        };
283      });
284      unlockInputs();
285    } else {
286      lockInputs();
287    }
288
289    prevBtn.disabled = idx === 0;
290    nextBtn.disabled = idx === questions.length - 1;
291  }
292
293  function buildReport() {
294    const totalQ = questions.length;
295    let correct = 0;
296    const items = questions.map((q, i) => {
297      const correctIdx = Number(q.correctAnswer);
298      const pickedIdx = typeof answers[i] === "number" ? answers[i] : null;
299      const ok = pickedIdx === correctIdx;
300      if (ok) correct += 1;
301      return {
302        i,
303        question: q.question || "",
304        options: Array.isArray(q.options) ? q.options : [],
305        correctIdx,
306        pickedIdx,
307        explanation: q.explanation || "",
308        ok,
309      };
310    });
311    const score = Math.round((correct / totalQ) * 100);
312    const pass = score >= 70;
313    return { correct, total: totalQ, score, pass, items };
314  }
315
316  function renderReport(report) {
317    const currentUrl = encodeURIComponent(window.location.href);
318    const shareText = encodeURIComponent(`I just scored ${report.score}% on the ${quizTitle} practice quiz on ITlearn360! Can you beat my score?`);
319    const twitterUrl = `https://twitter.com/intent/tweet?text=${shareText}&url=${currentUrl}`;
320    const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${currentUrl}`;
321    const upsellMessage = practiceMeta
322      ? report.pass
323        ? nextPackageAssessment
324          ? `You passed this assessment. Continue with the next assessment in ${packageDisplayTitle}.`
325          : `You passed the final assessment in ${packageDisplayTitle}. Review your full pack progress and completion rewards.`
326        : `Review the answers below, then return to ${packageDisplayTitle} when you are ready to continue.`
327      : report.pass
328        ? "Great job. Continue into the full course to deepen the skill and work through the complete learning path."
329        : "Review the gaps below, then use the full course to strengthen the topics that need more work.";
330
331    resultBox.style.display = "block";
332    resultBox.innerHTML = `
333      <section class="quizScoreCardV335 ${report.pass ? "pass" : "retry"}">
334        <div class="quizScoreLabelV335">${report.pass ? "Passed" : "Keep going"}</div>
335        <strong>${report.score}%</strong>
336        <p>${report.correct} of ${report.total} questions correct</p>
337        <div class="quizScoreCtaV335">
338          <p>${upsellMessage}</p>
339          <div>
340            ${practiceMeta ? `${nextPackageAssessment && report.pass ? `<button type="button" class="quizBtnV335 primary" data-next-package-assessment>Next assessment ${icon("arrow")}</button>` : ""}<button type="button" class="quizBtnV335 secondary" data-back-package>View full skill assessment pack</button>` : `<button type="button" class="quizBtnV335 primary" data-enroll-course>View full course</button>`}
341            <a href="${twitterUrl}" target="_blank" rel="noopener noreferrer" class="quizShareBtnV335 x">Share on X</a>
342            <a href="${linkedinUrl}" target="_blank" rel="noopener noreferrer" class="quizShareBtnV335 linkedin">Share on LinkedIn</a>
343          </div>
344        </div>
345      </section>
346    `;
347    resultBox.querySelector("[data-enroll-course]")?.addEventListener("click", () => navigate(`/course/${courseId}`));
348    resultBox.querySelector("[data-back-package]")?.addEventListener("click", () => { if (packageUrl) navigate(packageUrl); });
349    resultBox.querySelector("[data-next-package-assessment]")?.addEventListener("click", () => { if (nextPackageAssessment) navigate(quizHref(nextPackageAssessment)); });
350
351    reportBox.style.display = "block";
352    reportBox.innerHTML = `
353      <section class="quizDetailedReportV335">
354        <div class="quizDetailedReportHeadV335">
355          <div>
356            <div class="quizEyebrowV335">DETAILED REPORT</div>
357            <h3>Review each answer.</h3>
358          </div>
359          <span>${report.correct}/${report.total} correct</span>
360        </div>
361        <div class="quizReportItemsV335">
362          ${report.items
363            .map((it) => {
364              const yourAns = typeof it.pickedIdx === "number" ? it.options[it.pickedIdx] : "Not answered";
365              const correctAns = typeof it.correctIdx === "number" ? it.options[it.correctIdx] : "";
366              return `
367                <article class="quizReportItemV335 ${it.ok ? "correct" : "wrong"}">
368                  <div class="quizReportItemHeadV335">
369                    <span>${it.ok ? "CORRECT" : "REVIEW"}</span>
370                    <strong>${esc(it.question)}</strong>
371                  </div>
372                  <p class="quizYourAnswerV335">Your answer: ${esc(yourAns)}</p>
373                  ${!it.ok ? `<p class="quizCorrectAnswerV335">Correct answer: ${esc(correctAns)}</p>` : ""}
374                  ${it.explanation ? `<div class="quizExplanationV335"><b>Explanation:</b> ${esc(it.explanation)}</div>` : ""}
375                </article>
376              `;
377            })
378            .join("")}
379        </div>
380      </section>
381    `;
382  }
383
384  function refreshPackageProgress() {
385    if (!packageAssessments.length) return;
386    const passed = packageAssessments.filter((assessment) => getQuizAttempt(assessment.courseId, assessment.moduleId, assessment.quizId)?.pass).length;
387    const percent = Math.round((passed / packageAssessments.length) * 100);
388    const bar = root.querySelector("[data-package-progress-bar]");
389    const track = root.querySelector("[data-package-progress-track]");
390    const count = root.querySelector("[data-package-passed-count]");
391    const percentEl = root.querySelector("[data-package-progress-percent]");
392    if (bar) bar.style.width = `${percent}%`;
393    if (track) track.setAttribute("aria-label", `${percent}% of assessment pack completed`);
394    if (count) count.textContent = String(passed);
395    if (percentEl) percentEl.textContent = `${percent}%`;
396  }
397
398  function submit() {
399    if (!Array.isArray(questions) || !questions.length) return;
400    submitted = true;
401    const report = buildReport();
402    saveQuizAttempt(courseId, moduleId, quizId, {
403      score: report.score,
404      pass: report.pass,
405      correct: report.correct,
406      total: report.total,
407      submittedAt: new Date().toISOString(),
408      packageId: practiceMeta?.packageId || null,
409    });
410    refreshPackageProgress();
411    submitBtn.disabled = true;
412    submitBtn.style.display = "none";
413    retryBtn.style.display = "inline-flex";
414    renderQuestion();
415    renderReport(report);
416    resultBox.scrollIntoView({ behavior: "smooth", block: "center" });
417  }
418
419  function retry() {
420    idx = 0;
421    submitted = false;
422    Object.keys(answers).forEach((k) => delete answers[k]);
423    submitBtn.disabled = false;
424    submitBtn.style.display = "inline-flex";
425    retryBtn.style.display = "none";
426    resultBox.style.display = "none";
427    reportBox.style.display = "none";
428    resultBox.innerHTML = "";
429    reportBox.innerHTML = "";
430    renderQuestion();
431    root.querySelector("#pubQuizGrid")?.scrollIntoView({ behavior: "smooth", block: "start" });
432  }
433
434  prevBtn.onclick = () => {
435    if (idx > 0) {
436      idx -= 1;
437      renderQuestion();
438    }
439  };
440  nextBtn.onclick = () => {
441    if (idx < questions.length - 1) {
442      idx += 1;
443      renderQuestion();
444    }
445  };
446  submitBtn.onclick = submit;
447  retryBtn.onclick = retry;
448
449  renderQuestion();
450}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.