NewsQuiz
The week's news quiz: a question at a time, each pick marked right or wrong with the story behind it, then the score with a mark per question and Play again.
Demo
The week in news, Oct 30
Question 1 of 3Which city was picked on Tuesday to host the 2030 climate summit?
Not finished yet.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/news_quiz.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/news_quiz.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/news_quiz.qwe
component NewsQuizDemoLive() { state note = "Not finished yet." <div.demo-stack> <NewsQuiz id="news-quiz-demo" title="The week in news, Oct 30" on_finish={(score, total) => { note = `on_finish gave ${score} of ${total}` }} questions={[ { q: "Which city was picked on Tuesday to host the 2030 climate summit?", choices: ["Lisbon", "Nairobi", "Osaka", "Bogotá"], answer: 1, explain: "Nairobi will be the first African city to host it.", href: "#summit" }, { q: "By how much did the central bank change its main rate this week?", choices: ["Cut by 0.25 points", "Raised by 0.25 points", "No change"], answer: 2, explain: "It held at 4.25% for a fourth month." }, { q: "What did the city council vote to ban from the old town from January?", choices: ["Cars", "Tour buses", "E-scooters", "Short-term rentals"], answer: 2, href: "#scooters" }, ]}/> <p.demo-note id="news-quiz-demo-note">{note}</p> </div>}view NewsQuizDemo() = <NewsQuizDemoLive/>Source
ui/news_quiz.qwe
// NewsQuiz: the week's news quiz: one question at a time with its choices; a pick// is marked right or wrong at once, with the right answer and a line on the story// behind it; then the next question, and at the end the score, a mark for each// question and a word for how it went, with Play again. Screen readers hear each// verdict, and the focus goes to each new question.//// <NewsQuiz id="quiz-44" title="The week in news" questions={[// { q: "Which city will host the 2030 summit?", choices: ["Lisbon", "Nairobi", "Osaka"], answer: 1,// explain: "Nairobi was picked on Tuesday, the first African city to host it.", href: "/world/summit" },// ...// ]} on_finish={(score, total) => { save_score(score) }}/>//// questions: maps of { q, choices, answer, explain?, href? }: q the question;// choices its answers (two to six); answer the right one's place, from 0; explain a// line shown after a pick; href the story it comes from ("Read the story").// id (required): unique on the page, for the questions' ids. title: the quiz's// heading, an <h3> ("News quiz"). verdicts: what to say at the end, from best to// worst, for a full score, at least two thirds, at least one third, and less// (["Perfect score!", "Well read.", "Not bad.", "Time to catch up on the news."]).// on_finish(score, total): inside one of your components, given the number right// and the number of questions when the last is answered.import ui/theme.qwe use ui_clslet news_quiz_css = css`.ui-nquiz { display: grid; gap: 14px; max-width: 560px; padding: 18px 20px 20px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 15px/1.45 var(--ui-font); }.ui-nquiz-head { display: grid; gap: 8px; }.ui-nquiz-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px; }.ui-nquiz .ui-nquiz-title { margin: 0; font-size: 17px; font-weight: 750; }.ui-nquiz-count { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-nquiz-track { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px; }.ui-nquiz-track span { height: 4px; border-radius: 2px; background: var(--ui-border); }.ui-nquiz-track .ui-nquiz-r { background: var(--ui-success); }.ui-nquiz-track .ui-nquiz-w { background: var(--ui-danger); }.ui-nquiz-track .ui-nquiz-now { background: var(--ui-accent); }.ui-nquiz-q { margin: 0; font-size: 19px; line-height: 1.35; font-weight: 700; }.ui-nquiz-q:focus { outline: none; }.ui-nquiz-q:focus-visible { box-shadow: var(--ui-focus); border-radius: 4px; }.ui-nquiz-choices { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-nquiz-choice { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 46px; padding: 8px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 15px/1.35 var(--ui-font); text-align: left; cursor: pointer; transition: border-color .15s, background .15s; }.ui-nquiz-choice:hover:not([aria-disabled="true"]) { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-nquiz-choice:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-nquiz-choice[aria-disabled="true"] { cursor: default; }.ui-nquiz-letter { flex: none; display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--ui-subtle); color: var(--ui-muted); font-size: 13px; font-weight: 700; }.ui-nquiz-text { flex: 1; }.ui-nquiz-mark { flex: none; font-weight: 800; }.ui-nquiz-right { border-color: var(--ui-success); background: var(--ui-success-soft); }.ui-nquiz-right .ui-nquiz-letter { background: var(--ui-success); color: var(--ui-bg); }.ui-nquiz-right .ui-nquiz-mark { color: var(--ui-success); }.ui-nquiz-wrong { border-color: var(--ui-danger); background: var(--ui-danger-soft); }.ui-nquiz-wrong .ui-nquiz-letter { background: var(--ui-danger); color: var(--ui-bg); }.ui-nquiz-wrong .ui-nquiz-mark { color: var(--ui-danger); }.ui-nquiz-faded { opacity: .55; }.ui-nquiz-feedback { display: grid; gap: 4px; }.ui-nquiz-feedback:empty { margin-top: -14px; }.ui-nquiz-verdict { margin: 0; font-weight: 700; }.ui-nquiz-ok .ui-nquiz-verdict { color: var(--ui-success); }.ui-nquiz-no .ui-nquiz-verdict { color: var(--ui-danger); }.ui-nquiz-explain { margin: 0; color: var(--ui-muted); font-size: 14px; }.ui-nquiz-explain a { color: var(--ui-accent); font-weight: 600; }.ui-nquiz-btn { justify-self: start; height: 40px; padding: 0 18px; border: 0; border-radius: 999px; background: var(--ui-accent); color: var(--ui-accent-fg); font: 650 14px var(--ui-font); cursor: pointer; }.ui-nquiz-btn:hover { filter: brightness(1.08); }.ui-nquiz-btn:focus-visible, .ui-nquiz a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-nquiz-end { display: grid; justify-items: center; gap: 10px; padding: 8px 0 4px; text-align: center; }.ui-nquiz-score { margin: 0; font-size: 42px; line-height: 1; font-weight: 800; font-variant-numeric: tabular-nums; }.ui-nquiz-score small { color: var(--ui-muted); font-size: 20px; font-weight: 600; }.ui-nquiz-say { margin: 0; font-size: 17px; font-weight: 650; }.ui-nquiz-marks { display: flex; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-nquiz-marks li { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; font-size: 12px; font-weight: 800; color: var(--ui-bg); }.ui-nquiz-marks .ui-nquiz-r { background: var(--ui-success); }.ui-nquiz-marks .ui-nquiz-w { background: var(--ui-danger); }.ui-nquiz-end .ui-nquiz-btn { justify-self: center; }@media (prefers-reduced-motion: reduce) { .ui-nquiz-choice { transition: none; } }`component NewsQuiz(questions: list, id: str, title: str = "News quiz", verdicts: list = ["Perfect score!", "Well read.", "Not bad.", "Time to catch up on the news."], on_finish = null, class: str? = null) { style global news_quiz_css state at = 0 state picked = null state results = [] state done = false state focus = null let total = len(questions) let q = questions[at] let score = len(results | filter((r) => r)) let letters = ["A", "B", "C", "D", "E", "F"] let q_id = `${id}-q` if focus != null { every(16, () => { let el = js("document.getElementById", focus) focus = null if el != null { el.focus() } }) } let choose = (i: int) => { if picked == null { picked = i results = results + [i == q.answer] if at == total - 1 && on_finish != null { on_finish(len(results | filter((r) => r)), total) } } } let next = () => { if at < total - 1 { at += 1 picked = null focus = q_id } else { done = true focus = `${id}-score` } } let again = () => { at = 0 picked = null results = [] done = false focus = q_id } let say = score == total ? verdicts[0] : (score * 3 >= total * 2 ? verdicts[1] : (score * 3 >= total ? verdicts[2] : verdicts[3])) <section class={ui_cls(["ui-nquiz", class])} id={id} aria-labelledby={`${id}-title`}> <div.ui-nquiz-head> <div.ui-nquiz-row> <h3.ui-nquiz-title id={`${id}-title`}>{title}</h3> <span.ui-nquiz-count>{done ? `${total} questions` : `Question ${at + 1} of ${total}`}</span> </div> <div.ui-nquiz-track aria-hidden="true"> {for i in range(total) => <span class={i < len(results) ? (results[i] ? "ui-nquiz-r" : "ui-nquiz-w") : (i == at && !done ? "ui-nquiz-now" : null)}></span>} </div> </div> {done ? <div.ui-nquiz-end> <p.ui-nquiz-score id={`${id}-score`} tabindex="-1">{score}<small> / {total}</small><span.ui-sr> right</span></p> <p.ui-nquiz-say>{say}</p> <ol.ui-nquiz-marks aria-label="Your answers"> {for [i, r] in enumerate(results) => <li class={r ? "ui-nquiz-r" : "ui-nquiz-w"}> <span aria-hidden="true">{r ? "✓" : "✗"}</span><span.ui-sr>Question {i + 1}: {r ? "right" : "wrong"}</span> </li>} </ol> <button.ui-nquiz-btn type="button" @click={again}>Play again</button> </div> : [ <p.ui-nquiz-q id={q_id} tabindex="-1">{q.q}</p>, <ul.ui-nquiz-choices role="list" aria-labelledby={q_id}> {for [i, c] in enumerate(q.choices) => { let right = picked != null && i == q.answer let wrong = picked != null && i == picked && i != q.answer <li key={`${at}-${i}`}> <button type="button" data-choice={i} class={ui_cls(["ui-nquiz-choice", right && "ui-nquiz-right", wrong && "ui-nquiz-wrong", picked != null && !right && !wrong && "ui-nquiz-faded"])} aria-disabled={picked != null ? "true" : null} @click={() => choose(i)}> <span.ui-nquiz-letter aria-hidden="true">{letters[i] ?? ""}</span> <span.ui-nquiz-text>{c}</span> {right && <span.ui-nquiz-mark><span aria-hidden="true">✓</span><span.ui-sr>, the right answer</span></span>} {wrong && <span.ui-nquiz-mark><span aria-hidden="true">✗</span><span.ui-sr>, wrong</span></span>} {picked == i && <span.ui-sr>, your answer</span>} </button> </li> }} </ul>, <div class={ui_cls(["ui-nquiz-feedback", picked != null && (picked == q.answer ? "ui-nquiz-ok" : "ui-nquiz-no")])} role="status"> {picked != null && [ <p.ui-nquiz-verdict>{picked == q.answer ? "Right!" : `Not quite: it was ${letters[q.answer] ?? ""}, ${q.choices[q.answer]}.`}</p>, (q.explain != null || q.href != null) && <p.ui-nquiz-explain> {q.explain}{q.explain != null && q.href != null && " "}{q.href != null && <a href={q.href}>Read the story</a>} </p>, ]} </div>, picked != null && <button.ui-nquiz-btn type="button" @click={next}>{at < total - 1 ? "Next question" : "See your score"}</button>, ]} </section>}Related
AdSlotAn advertisement's place kept at its size so the story never jumps: standard sizes by name, another on phones, a clear label, a placeholder, sticky in a rail.6 views · 8 downloadsArticleHeaderThe top of a story: its label and section, the headline, the dek, every author grouped by role, published and updated times, and the reading time.8 views · 8 downloadsBreakingNewsA breaking-news banner: the label in a block of colour, the headline and a line more, how long ago it came in, kept up to date, and a close that can be remembered.9 views · 8 downloadsCorrectionNoticeA correction, clarification, update, editor's note or retraction on a story, dated, with a link to the corrections policy; boxed on top or a line at the end.8 views · 7 downloadsElectionResultsOne race's results: candidates in party colours with votes and share bars, the share of the vote counted, the margin, and the winner once it is called.8 views · 8 downloadsExtractiveSummaryKeeps a text's best sentences, scored by word frequency and position, in their order, with a length slider and the chosen ones marked. No AI service.2 views · 6 downloads
Thread
Thread 0 posts
On NewsQuiz: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.