domma-cms 1.8.31 → 1.8.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +1 -1
  3. package/plugins/_lib/admin/games/games.css +1 -0
  4. package/plugins/_lib/admin/games/kit.js +241 -0
  5. package/plugins/_lib/games/http.js +82 -0
  6. package/plugins/_lib/games/names.js +42 -0
  7. package/plugins/_lib/games/plays.js +155 -0
  8. package/plugins/_lib/games/ranking.js +68 -0
  9. package/plugins/_lib/games/timing.js +27 -0
  10. package/plugins/_lib/public/games/format.js +38 -0
  11. package/plugins/_lib/public/games/games.css +1 -0
  12. package/plugins/_lib/public/games/games.js +204 -0
  13. package/plugins/crossword/admin/css/index.css +1 -0
  14. package/plugins/crossword/admin/templates/crossword.html +1 -0
  15. package/plugins/crossword/admin/views/crossword.js +317 -0
  16. package/plugins/crossword/collections/crossword-plays.json +20 -0
  17. package/plugins/crossword/collections/crossword-puzzles.json +17 -0
  18. package/plugins/crossword/config.js +6 -0
  19. package/plugins/crossword/inject/body.html +1 -0
  20. package/plugins/crossword/lib/crossword.js +170 -0
  21. package/plugins/crossword/lib/store.js +56 -0
  22. package/plugins/crossword/plugin.js +241 -0
  23. package/plugins/crossword/plugin.json +78 -0
  24. package/plugins/crossword/public/crossword.css +1 -0
  25. package/plugins/crossword/public/crossword.js +57 -0
  26. package/plugins/crossword/public/lib/layout.js +199 -0
  27. package/plugins/quiz/admin/css/index.css +1 -0
  28. package/plugins/quiz/admin/templates/quiz.html +1 -0
  29. package/plugins/quiz/admin/views/quiz.js +352 -0
  30. package/plugins/quiz/collections/quiz-plays.json +20 -0
  31. package/plugins/quiz/collections/quiz-quizzes.json +17 -0
  32. package/plugins/quiz/config.js +6 -0
  33. package/plugins/quiz/inject/body.html +1 -0
  34. package/plugins/quiz/lib/engine.js +115 -0
  35. package/plugins/quiz/lib/quiz.js +225 -0
  36. package/plugins/quiz/lib/store.js +56 -0
  37. package/plugins/quiz/plugin.js +241 -0
  38. package/plugins/quiz/plugin.json +78 -0
  39. package/plugins/quiz/public/quiz.css +1 -0
  40. package/plugins/quiz/public/quiz.js +80 -0
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "quiz",
3
+ "displayName": "Quiz",
4
+ "version": "1.0.0",
5
+ "tier": "free",
6
+ "description": "Make quizzes in the admin and put them on any page. Players type a name, answer against the clock, and see where they came on the leaderboard. The server keeps the time and marks every answer, so the browser never holds the answers.",
7
+ "author": "Domma CMS",
8
+ "date": "2026-10-20",
9
+ "icon": "help-circle",
10
+ "permissions": [
11
+ {
12
+ "key": "quiz",
13
+ "label": "Quiz",
14
+ "group": "Plugins",
15
+ "icon": "help-circle",
16
+ "description": "See quizzes and who played them (read), or make, change and remove quizzes and plays (manage).",
17
+ "actions": [
18
+ {
19
+ "key": "read",
20
+ "label": "See",
21
+ "description": "Open Quiz and see the quizzes, plays and leaderboards"
22
+ },
23
+ {
24
+ "key": "manage",
25
+ "label": "Manage",
26
+ "description": "Make, change and delete quizzes; hide or remove plays; reset a leaderboard"
27
+ }
28
+ ],
29
+ "grant": [
30
+ "admin"
31
+ ]
32
+ }
33
+ ],
34
+ "inject": {
35
+ "bodyEnd": "inject/body.html"
36
+ },
37
+ "admin": {
38
+ "css": [
39
+ "../_lib/admin/games/games.css",
40
+ "admin/css/index.css"
41
+ ],
42
+ "sidebar": [
43
+ {
44
+ "id": "quiz",
45
+ "text": "Quiz",
46
+ "icon": "help-circle",
47
+ "url": "#/plugins/quiz",
48
+ "section": "#/plugins/quiz",
49
+ "permission": "quiz",
50
+ "group": "Games",
51
+ "countUrl": "/api/plugins/quiz/count"
52
+ }
53
+ ],
54
+ "routes": [
55
+ {
56
+ "path": "/plugins/quiz",
57
+ "view": "plugin-quiz",
58
+ "title": "Quiz - Domma CMS"
59
+ },
60
+ {
61
+ "path": "/plugins/quiz/:a",
62
+ "view": "plugin-quiz",
63
+ "title": "Quiz - Domma CMS"
64
+ },
65
+ {
66
+ "path": "/plugins/quiz/:a/:b",
67
+ "view": "plugin-quiz",
68
+ "title": "Quiz - Domma CMS"
69
+ }
70
+ ],
71
+ "views": {
72
+ "plugin-quiz": {
73
+ "entry": "quiz/admin/views/quiz.js?v=5be1f180",
74
+ "exportName": "quizView"
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1 @@
1
+ .qz-q{font-size:1.35rem;line-height:1.35;margin:0 0 1rem;outline:none}.qz-image{display:block;max-width:100%;max-height:320px;border-radius:8px;margin:0 0 1rem}.qz-hint{margin:-.5rem 0 .75rem}.qz-options{display:grid;gap:.6rem}@media(min-width:640px){.qz-options{grid-template-columns:1fr 1fr}}.qz-option{display:flex;align-items:center;gap:.75rem;width:100%;text-align:left;font:inherit;cursor:pointer;padding:.8rem 1rem;border-radius:10px;border:2px solid var(--gm-line);background:var(--gm-soft);color:inherit;transition:border-color .12s,transform .12s}.qz-option:hover:not(:disabled){border-color:var(--gm-accent);transform:translateY(-1px)}.qz-option:focus-visible{outline:2px solid var(--gm-accent);outline-offset:2px}.qz-option:disabled{cursor:default;opacity:.7}.qz-option.is-picked{border-color:var(--gm-accent);background:color-mix(in srgb,var(--gm-accent) 15%,transparent)}.qz-letter{flex:none;display:inline-grid;place-items:center;width:1.8rem;height:1.8rem;border-radius:50%;background:var(--gm-accent);color:#fff;font-weight:700;font-size:.9rem}.qz-typed{display:flex;flex-direction:column;gap:.35rem;max-width:420px}.qz-verdict{font-size:1.8rem;margin:0 0 .5rem;outline:none}.qz-verdict.is-right{color:var(--gm-good)}.qz-verdict.is-wrong{color:var(--gm-bad)}.qz-review{margin-top:1.5rem}.qz-review h3{margin:0 0 .5rem;font-size:1.1rem}.qz-review-list{margin:0;padding-left:0;list-style:none;display:grid;gap:.5rem}.qz-review-list li{padding:.6rem .8rem;border-radius:8px;border-left:4px solid var(--gm-line);background:var(--gm-soft)}.qz-review-list li.is-right{border-left-color:var(--gm-good)}.qz-review-list li.is-wrong{border-left-color:var(--gm-bad)}.qz-review-q{margin:0 0 .25rem;font-weight:600}.qz-review-a{margin:0}.qz-live{position:absolute;left:-9999px}
@@ -0,0 +1,80 @@
1
+ import{Countdown as C,addStyles as N,celebrate as Y,configOf as Q,fmtDuration as $,memory as f,mountBoards as W,mountLeaderboard as B,ordinal as I,request as h,uid as D}from"/plugins/_lib/public/games/games.js";const s=window.M,c="/api/plugins/quiz/public",A="games.name",z=n=>`quiz.play.${n}`,F=`
2
+ <p class="gm-muted qz-live" role="status" aria-live="polite" data-bind-text="live.value"></p>
3
+
4
+ <section class="gm-card" hidden data-bind-hidden="screen.value !== 'loading'"><p class="gm-muted">Loading the quiz\u2026</p></section>
5
+
6
+ <section class="gm-card" hidden data-bind-hidden="screen.value !== 'error'">
7
+ <p class="gm-error" data-bind-text="problem.value"></p>
8
+ </section>
9
+
10
+ <section class="gm-card qz-intro" hidden data-bind-hidden="screen.value !== 'intro'">
11
+ <h2 class="gm-title" data-bind-text="info.value.title"></h2>
12
+ <p class="gm-intro" hidden data-bind-hidden="!info.value.intro" data-bind-text="info.value.intro"></p>
13
+ <ul class="gm-facts" data-each="facts.value key=k"><li data-bind-text="k"></li></ul>
14
+ <p class="gm-note" hidden data-bind-hidden="info.value.open" data-bind-text="info.value.reason"></p>
15
+ <form class="gm-start" hidden data-bind-hidden="!info.value.open" data-on-submit="start" novalidate>
16
+ <div class="gm-field">
17
+ <label for="%U%-name">Your name</label>
18
+ <input id="%U%-name" class="gm-input" maxlength="24" autocomplete="nickname" data-model="name.value"
19
+ data-bind-aria-invalid="problem.value ? 'true' : false" aria-describedby="%U%-err">
20
+ </div>
21
+ <div class="gm-hp" aria-hidden="true"><label>Website <input tabindex="-1" autocomplete="off" data-model="website.value"></label></div>
22
+ <button type="submit" class="btn btn-primary" data-bind-disabled="busy.value">Start the quiz</button>
23
+ </form>
24
+ <p class="gm-error" id="%U%-err" hidden data-bind-hidden="!problem.value" data-bind-text="problem.value"></p>
25
+ </section>
26
+
27
+ <section class="gm-card qz-question" hidden data-bind-hidden="screen.value !== 'question'" aria-labelledby="%U%-q">
28
+ <div class="gm-bar">
29
+ <span class="gm-progress" data-bind-text="progress.value"></span>
30
+ <span class="gm-clock" hidden data-bind-hidden="!clockShown.value" data-bind-class="clockLow.value && 'is-low'"
31
+ role="timer" data-bind-aria-label="'Time left ' + clockText.value" data-bind-text="clockText.value"></span>
32
+ </div>
33
+ <div class="gm-track" aria-hidden="true"><span data-bind-style-width="trackWidth.value"></span></div>
34
+ <h2 class="qz-q" id="%U%-q" tabindex="-1" data-bind-text="q.value.text"></h2>
35
+ <img class="qz-image" alt="" hidden data-bind-hidden="!q.value.image" data-bind-src="q.value.image || false">
36
+ <p class="gm-muted qz-hint" hidden data-bind-hidden="q.value.kind !== 'multiple'">Choose every right answer, then press Answer.</p>
37
+ <div class="qz-options" hidden data-bind-hidden="q.value.kind === 'text'" data-each="options.value key=id">
38
+ <button type="button" class="qz-option" data-bind-class="picked && 'is-picked'" data-bind-aria-pressed="picked ? 'true' : 'false'"
39
+ data-bind-disabled="$root.busy.value" data-on-click="$root.choose($data)">
40
+ <span class="qz-letter" data-bind-text="letter"></span><span data-bind-text="text"></span>
41
+ </button>
42
+ </div>
43
+ <form class="qz-typed" hidden data-bind-hidden="q.value.kind !== 'text'" data-on-submit="sendTyped" novalidate>
44
+ <label class="gm-muted" for="%U%-typed">Your answer</label>
45
+ <input id="%U%-typed" class="gm-input" maxlength="200" autocomplete="off" data-model="typed.value">
46
+ </form>
47
+ <div class="gm-actions" hidden data-bind-hidden="q.value.kind !== 'multiple' && q.value.kind !== 'text'">
48
+ <button type="button" class="btn btn-primary" data-bind-disabled="busy.value" data-on-click="sendPicked">Answer</button>
49
+ </div>
50
+ </section>
51
+
52
+ <section class="gm-card qz-feedback" hidden data-bind-hidden="screen.value !== 'feedback'" aria-labelledby="%U%-fb">
53
+ <h2 class="qz-verdict" id="%U%-fb" tabindex="-1" data-bind-class="fb.value.correct ? 'is-right' : 'is-wrong'" data-bind-text="fb.value.title"></h2>
54
+ <p hidden data-bind-hidden="fb.value.correct || !fb.value.rightAnswer">The answer was <strong data-bind-text="fb.value.rightAnswer"></strong>.</p>
55
+ <p class="gm-muted" hidden data-bind-hidden="!fb.value.explanation" data-bind-text="fb.value.explanation"></p>
56
+ <div class="gm-actions"><button type="button" class="btn btn-primary" data-on-click="next" data-bind-text="fb.value.nextLabel"></button></div>
57
+ </section>
58
+
59
+ <section class="gm-card qz-result" hidden data-bind-hidden="screen.value !== 'result'" aria-labelledby="%U%-res">
60
+ <h2 class="gm-title" id="%U%-res" tabindex="-1" data-bind-text="res.value.title"></h2>
61
+ <p class="gm-result-score" data-bind-text="res.value.scoreText"></p>
62
+ <p class="gm-muted" data-bind-text="res.value.detail"></p>
63
+ <p class="gm-result-place" hidden data-bind-hidden="!res.value.placeText" data-bind-text="res.value.placeText"></p>
64
+ <div class="gm-actions">
65
+ <button type="button" class="btn btn-primary" data-on-click="again">Play again</button>
66
+ </div>
67
+ <div class="qz-review" hidden data-bind-hidden="!review.value.length">
68
+ <h3>Your answers</h3>
69
+ <ol class="qz-review-list" data-each="review.value key=k">
70
+ <li data-bind-class="correct ? 'is-right' : 'is-wrong'">
71
+ <p class="qz-review-q" data-bind-text="text"></p>
72
+ <p class="qz-review-a"><span data-bind-text="mark"></span> <span data-bind-text="yoursText"></span></p>
73
+ <p class="qz-review-a" hidden data-bind-hidden="correct">Answer: <strong data-bind-text="right"></strong></p>
74
+ <p class="gm-muted" hidden data-bind-hidden="!explanation" data-bind-text="explanation"></p>
75
+ </li>
76
+ </ol>
77
+ </div>
78
+ </section>
79
+
80
+ <div class="qz-board-slot"></div>`,H="ABCDEFGH";function K(n){const l=[`${n.questions} question${n.questions===1?"":"s"}`];return n.timeLimit&&l.push(`${$(n.timeLimit*1e3)} to finish`),n.questionSeconds?l.push(`${n.questionSeconds} seconds a question`):n.perQuestionTimes&&l.push("Some questions are timed"),n.attemptsPerName===1?l.push("One go each"):n.attemptsPerName>1&&l.push(`${n.attemptsPerName} goes each`),n.leaderboard&&l.push("Fastest top score wins"),l.map(g=>({k:g}))}function G(n){const l=Q(n,"data-quiz");if(!l?.slug||!s)return;N(["/plugins/quiz/public/quiz.css?v=1.0.0"]);const g=D("qz");n.innerHTML=F.replaceAll("%U%",g);const t={screen:s.observable("loading"),live:s.observable(""),problem:s.observable(""),busy:s.observable(!1),info:s.observable({title:"",intro:"",open:!1,reason:""}),facts:s.observable([]),name:s.observable(String(f.get(A)||"")),website:s.observable(""),progress:s.observable(""),q:s.observable({text:"",kind:"single",image:""}),options:s.observable([]),typed:s.observable(""),clockText:s.observable(""),clockShown:s.observable(!1),clockLow:s.observable(!1),trackWidth:s.observable("0%"),fb:s.observable({}),res:s.observable({}),review:s.observable([])};let r=f.get(z(l.slug))||null,k=null,d=null,u=null,m=null;const b=e=>setTimeout(()=>n.querySelector(`#${g}-${e}`)?.focus({preventScroll:!1}),30),q=e=>{t.live.value="",setTimeout(()=>{t.live.value=e},20)};function T(){const e=u?.left.peek()??0,i=m?.left.peek()??0,a=d?.questionLeftMs!=null&&(d?.totalLeftMs==null||e<=i),o=a?e:i,M=a?(d?.question?.seconds||0)*1e3:(t.info.peek().timeLimit||0)*1e3;t.clockShown.value=d?.questionLeftMs!=null||d?.totalLeftMs!=null,t.clockText.value=$(o),t.clockLow.value=o>0&&o<=5e3,t.trackWidth.value=M?`${Math.max(0,Math.min(100,o/M*100)).toFixed(1)}%`:`${(d?.index||0)/(d?.total||1)*100}%`}function y(){u?.stop(),m?.stop()}u=new C(()=>{t.screen.peek()==="question"&&v(null,{timeout:!0})}),m=new C(()=>{["question","feedback"].includes(t.screen.peek())&&setTimeout(p,600)});const R=s.effect(()=>{u.left.value,m.left.value,T()});function x(){f.remove(z(l.slug)),r=null}async function L(){const e=await h(`${c}/quiz/${encodeURIComponent(l.slug)}`);return t.info.value=e,t.facts.value=K(e),e.leaderboard&&!k&&(k=B(n.querySelector(".qz-board-slot"),{url:`${c}/quiz/${encodeURIComponent(l.slug)}/leaderboard`,limit:e.leaderboardSize,title:"Leaderboard"})),e}async function p(){if(!r){t.screen.value="intro";return}try{const e=await h(`${c}/play/${encodeURIComponent(r.playId)}`,{token:r.token});if(d=e,e.status!=="playing")return y(),w();E(e)}catch(e){if(e.code==="gone"||e.status===404){x(),t.screen.value="intro";return}t.problem.value=e.message,t.screen.value="error"}}function E(e){const i=e.question;t.q.value={text:i.text,kind:i.kind,image:i.image||""},t.options.value=i.options.map((a,o)=>({id:a.id,text:a.text,letter:H[o]||"",picked:!1})),t.typed.value="",t.progress.value=`Question ${e.index+1} of ${e.total}`,t.busy.value=!1,t.screen.value="question",u.start(e.questionLeftMs??0),m.start(e.totalLeftMs??0),T(),i.kind==="text"?b("typed"):b("q"),q(`Question ${e.index+1} of ${e.total}. ${i.text}`)}async function v(e,{timeout:i=!1}={}){if(!(t.busy.peek()||!d?.question)){t.busy.value=!0,u.stop();try{const a=await h(`${c}/play/${encodeURIComponent(r.playId)}/answer`,{method:"POST",token:r.token,body:{q:d.question.id,answer:e}});d=a.play;const o=a.play.status!=="playing";if(a.feedback){t.fb.value={correct:a.correct,title:a.correct?"Right!":i||a.late?"Out of time":"Not quite",rightAnswer:a.feedback.rightAnswer,explanation:a.feedback.explanation,nextLabel:o?"See your score":"Next question"},t.screen.value="feedback",q(t.fb.peek().title),b("fb");return}if(o)return y(),w();await p()}catch(a){if(a.code==="not-current"||a.code==="finished")return p();t.busy.value=!1,t.live.value=a.message}}}async function w(){y();try{const e=await h(`${c}/play/${encodeURIComponent(r.playId)}/result`,{token:r.token}),i=e.place?`You came ${I(e.place)} of all time${e.placeToday&&e.placeToday!==e.place?`, ${I(e.placeToday)} today`:""}.`:"";t.res.value={title:e.timeUp?`Time's up, ${e.name}!`:`Well done, ${e.name}!`,scoreText:`${e.score} / ${e.maxScore}`,detail:`${e.correct} of ${e.total} right in ${$(e.timeMs)}.`,placeText:i},t.review.value=(e.review||[]).map((a,o)=>({k:`r${o}`,text:`${o+1}. ${a.text}`,correct:a.correct,right:a.right,explanation:a.explanation,mark:a.correct?"\u2713":"\u2717",yoursText:a.yours?`You said: ${a.yours}`:a.late?"Out of time":"No answer"})),t.screen.value="result",q(`${t.res.peek().title} ${t.res.peek().detail} ${i}`),b("res"),k?.refresh(r.playId),e.place===1&&Y()}catch(e){if(e.status===404){x(),t.screen.value="intro";return}t.problem.value=e.message,t.screen.value="error"}}async function O(){if(!t.busy.peek()){t.problem.value="",t.busy.value=!0;try{const e=await h(`${c}/quiz/${encodeURIComponent(l.slug)}/start`,{method:"POST",body:{name:t.name.peek(),website:t.website.peek()}});f.set(A,e.name),r={playId:e.playId,token:e.token},f.set(z(l.slug),r),await p()}catch(e){t.problem.value=e.message,t.busy.value=!1,b("name")}}}const U={start(){return O(),!1},choose(e){if(!t.busy.peek()){if(t.q.peek().kind==="multiple"){t.options.value=t.options.peek().map(i=>i.id===e.id?{...i,picked:!i.picked}:i);return}v(e.id)}},sendPicked(){if(t.q.peek().kind==="text")return v(t.typed.peek());v(t.options.peek().filter(e=>e.picked).map(e=>e.id))},sendTyped(){return v(t.typed.peek()),!1},next(){if(d?.status!=="playing")return w();p()},async again(){x(),t.problem.value="",t.busy.value=!1,await L().catch(()=>{}),t.screen.value="intro",b("name")}};s.applyBindings({...t,...U},n,{methods:U}),L().then(p).catch(e=>{t.problem.value=e.status===404?"This quiz is not available.":e.message,t.screen.value="error"});const S=new MutationObserver(()=>{n.isConnected||(y(),R?.(),S.disconnect())});S.observe(document.body,{childList:!0,subtree:!0})}function P(){for(const n of document.querySelectorAll("[data-quiz]"))n.dataset.gmMounted||(n.dataset.gmMounted="1",G(n));W("data-quiz-board",n=>`${c}/quiz/${encodeURIComponent(n)}/leaderboard`)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",P):P();