@olinfo/quizms-training 5.0.0

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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Alessandro Bortolin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ import { studentSchema as i, calcScore as f } from "@olinfo/quizms/models";
2
+ import { validate as l } from "@olinfo/quizms/utils";
3
+ import { sumBy as p, cloneDeepWith as g, isDate as I } from "lodash-es";
4
+ import n from "zod";
5
+ async function D() {
6
+ if (window.parent === window) return "Quizms";
7
+ for (let e = 0; e < 50; e++)
8
+ try {
9
+ return await r({ type: "getTitle" }, n.string(), 200);
10
+ } catch {
11
+ }
12
+ throw new Error("Failed to connect");
13
+ }
14
+ async function _([e, t]) {
15
+ return window.parent === window ? null : await r({ type: "getStudent", contestId: t }, i.nullable());
16
+ }
17
+ function j(e) {
18
+ return async (t, s) => window.parent === window ? null : await r(
19
+ {
20
+ type: "setAnswers",
21
+ contestId: s.id,
22
+ answers: t.answers,
23
+ score: f(t, e.schema),
24
+ maxScore: p(Object.values(e.schema).map((a) => a.pointsCorrect))
25
+ },
26
+ i
27
+ );
28
+ }
29
+ function x(e) {
30
+ return async (t, s) => window.parent === window ? null : await r(
31
+ {
32
+ type: "start",
33
+ contestId: s.id,
34
+ variantId: t.variantId,
35
+ duration: s.onlineSettings?.duration ?? 0,
36
+ score: f(t, e.schema),
37
+ maxScore: p(Object.values(e.schema).map((a) => a.pointsCorrect))
38
+ },
39
+ i
40
+ );
41
+ }
42
+ async function A(e, t) {
43
+ return window.parent === window ? null : await r({ type: "submit", contestId: t.id }, i);
44
+ }
45
+ async function C(e, t) {
46
+ return window.parent === window ? null : await r({ type: "reset", contestId: t.id }, i);
47
+ }
48
+ async function E() {
49
+ return window.parent === window ? null : await r({ type: "logout" }, n.null());
50
+ }
51
+ async function r(e, t, s = 1e4) {
52
+ const a = crypto.randomUUID(), d = new AbortController(), y = new Promise((c, u) => {
53
+ window.addEventListener(
54
+ "message",
55
+ (w) => {
56
+ try {
57
+ const o = l(
58
+ h,
59
+ g(w.data, (m) => I(m) ? new Date(m) : void 0)
60
+ );
61
+ if (o.messageId !== a) return;
62
+ o.success ? c(l(t, o.data)) : u(new Error(o.error));
63
+ } catch (o) {
64
+ u(o);
65
+ }
66
+ },
67
+ { signal: d.signal }
68
+ );
69
+ });
70
+ window.parent.postMessage({ ...e, messageId: a }, "*");
71
+ try {
72
+ const c = new Promise((u, w) => setTimeout(() => w(), s));
73
+ return await Promise.race([y, c]);
74
+ } finally {
75
+ d.abort();
76
+ }
77
+ }
78
+ const h = n.discriminatedUnion("success", [
79
+ n.object({
80
+ success: n.literal(!0),
81
+ messageId: n.string(),
82
+ data: n.any()
83
+ }),
84
+ n.object({
85
+ success: n.literal(!1),
86
+ messageId: n.string(),
87
+ error: n.string()
88
+ })
89
+ ]);
90
+ export {
91
+ _ as getStudentIframe,
92
+ D as getTitleIframe,
93
+ E as logoutIframe,
94
+ C as resetIframe,
95
+ j as setAnswersIframe,
96
+ x as startIframe,
97
+ A as submitIframe
98
+ };
99
+ //# sourceMappingURL=iframe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iframe.js","sources":["../../src/entry/iframe.ts"],"sourcesContent":["import {\n type Contest,\n calcScore,\n type Student,\n studentSchema,\n type Variant,\n} from \"@olinfo/quizms/models\";\nimport { validate } from \"@olinfo/quizms/utils\";\nimport { cloneDeepWith, isDate, sumBy } from \"lodash-es\";\nimport z from \"zod\";\n\nexport async function getTitleIframe(): Promise<string> {\n if (window.parent === window) return \"Quizms\";\n\n for (let i = 0; i < 50; i++) {\n try {\n return await getResponse({ type: \"getTitle\" }, z.string(), 200);\n } catch {}\n }\n throw new Error(\"Failed to connect\");\n}\n\nexport async function getStudentIframe([_, contestId]: [string, string]): Promise<Student | null> {\n if (window.parent === window) return null;\n return await getResponse({ type: \"getStudent\", contestId }, studentSchema.nullable());\n}\n\nexport function setAnswersIframe(variant: Variant) {\n return async (student: Student, contest: Contest): Promise<Student | null> => {\n if (window.parent === window) return null;\n\n return await getResponse(\n {\n type: \"setAnswers\",\n contestId: contest.id,\n answers: student.answers,\n score: calcScore(student, variant.schema),\n maxScore: sumBy(Object.values(variant.schema).map((problem) => problem.pointsCorrect)),\n },\n studentSchema,\n );\n };\n}\n\nexport function startIframe(variant: Variant) {\n return async (student: Student, contest: Contest): Promise<Student | null> => {\n if (window.parent === window) return null;\n\n return await getResponse(\n {\n type: \"start\",\n contestId: contest.id,\n variantId: student.variantId,\n duration: contest.onlineSettings?.duration ?? 0,\n score: calcScore(student, variant.schema),\n maxScore: sumBy(Object.values(variant.schema).map((problem) => problem.pointsCorrect)),\n },\n studentSchema,\n );\n };\n}\n\nexport async function submitIframe(_student: Student, contest: Contest): Promise<Student | null> {\n if (window.parent === window) return null;\n return await getResponse({ type: \"submit\", contestId: contest.id }, studentSchema);\n}\n\nexport async function resetIframe(_student: Student, contest: Contest): Promise<Student | null> {\n if (window.parent === window) return null;\n return await getResponse({ type: \"reset\", contestId: contest.id }, studentSchema);\n}\n\nexport async function logoutIframe(): Promise<null> {\n if (window.parent === window) return null;\n return await getResponse({ type: \"logout\" }, z.null());\n}\n\nasync function getResponse<Message extends { type: string }, Data>(\n message: Message,\n schema: z.ZodType<Data>,\n timeout = 10_000,\n): Promise<Data> {\n const messageId = crypto.randomUUID();\n\n const controller = new AbortController();\n const responsePromise = new Promise<Data>((resolve, reject) => {\n window.addEventListener(\n \"message\",\n (ev) => {\n try {\n const response = validate(\n messageSchema,\n cloneDeepWith(ev.data, (value) => (isDate(value) ? new Date(value) : undefined)),\n );\n if (response.messageId !== messageId) return;\n\n if (response.success) {\n resolve(validate(schema, response.data));\n } else {\n reject(new Error(response.error));\n }\n } catch (err) {\n reject(err);\n }\n },\n { signal: controller.signal },\n );\n });\n\n window.parent.postMessage({ ...message, messageId }, \"*\");\n\n try {\n const timeoutPromise = new Promise<never>((_, reject) => setTimeout(() => reject(), timeout));\n return await Promise.race([responsePromise, timeoutPromise]);\n } finally {\n controller.abort();\n }\n}\n\nconst messageSchema = z.discriminatedUnion(\"success\", [\n z.object({\n success: z.literal(true),\n messageId: z.string(),\n data: z.any(),\n }),\n z.object({\n success: z.literal(false),\n messageId: z.string(),\n error: z.string(),\n }),\n]);\n"],"names":["getTitleIframe","i","getResponse","z","getStudentIframe","_","contestId","studentSchema","setAnswersIframe","variant","student","contest","calcScore","sumBy","problem","startIframe","submitIframe","_student","resetIframe","logoutIframe","message","schema","timeout","messageId","controller","responsePromise","resolve","reject","ev","response","validate","messageSchema","cloneDeepWith","value","isDate","err","timeoutPromise"],"mappings":"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"}
@@ -0,0 +1,131 @@
1
+ import { jsx as u } from "react/jsx-runtime";
2
+ import { useState as V, useMemo as v, useCallback as r } from "react";
3
+ import { TitleProvider as j } from "@olinfo/quizms/components";
4
+ import { variantSchema as A } from "@olinfo/quizms/models";
5
+ import { StudentProvider as D } from "@olinfo/quizms/student";
6
+ import { Rng as k, validate as x } from "@olinfo/quizms/utils";
7
+ import { addMinutes as R, subSeconds as C } from "date-fns";
8
+ import f, { preload as E } from "swr";
9
+ import { getTitleIframe as M, getStudentIframe as $, setAnswersIframe as z, submitIframe as F, resetIframe as N, logoutIframe as O, startIframe as U } from "./iframe.js";
10
+ import { TrainingStatementContext as q } from "./statement.js";
11
+ function B({
12
+ contest: a,
13
+ children: c
14
+ }) {
15
+ const { data: d } = f("getTitle", M, { suspense: !0 }), { data: o, mutate: l } = f(["student", a.id], $, {
16
+ suspense: !0
17
+ }), [p, I] = V(
18
+ G(a)
19
+ ), { student: s, type: w } = v(
20
+ () => o ? { type: "iframe", student: o } : { type: "anonymous", student: p },
21
+ [o, p]
22
+ ), { data: m } = f(
23
+ s.variantId && ["variant", a.id, s.variantId],
24
+ y,
25
+ {
26
+ revalidateOnFocus: !1,
27
+ revalidateIfStale: !1
28
+ }
29
+ ), n = r(
30
+ async (t, i) => {
31
+ w === "anonymous" ? I(i) : await l((e) => e ? t(i(e), a) : null, {
32
+ optimisticData: (e) => e ? i(e) : null
33
+ });
34
+ },
35
+ [w, a, l]
36
+ ), S = v(
37
+ () => ({
38
+ id: "",
39
+ schoolId: "",
40
+ contestId: a.id,
41
+ name: "",
42
+ participationWindow: s.participationWindow,
43
+ finalized: !1,
44
+ disabled: !1,
45
+ pdfVariants: []
46
+ }),
47
+ [a.id, s.participationWindow]
48
+ ), g = r(
49
+ async (t, i) => {
50
+ if (!m) throw new Error("Variant non loaded");
51
+ await n(
52
+ z(m),
53
+ (e) => ({ ...e, answers: { ...e.answers, [t]: i } })
54
+ );
55
+ },
56
+ [n, m]
57
+ ), h = r(async () => {
58
+ await n(
59
+ F,
60
+ (t) => ({
61
+ ...t,
62
+ participationWindow: { start: t.participationWindow.start, end: /* @__PURE__ */ new Date() }
63
+ })
64
+ );
65
+ }, [n]), W = r(async () => {
66
+ await n(
67
+ N,
68
+ (t) => ({
69
+ ...t,
70
+ answers: {},
71
+ participationWindow: void 0
72
+ })
73
+ );
74
+ }, [n]), T = r(async () => {
75
+ await n(O, (t) => t);
76
+ }, [n]), b = r(async () => {
77
+ const t = /* @__PURE__ */ new Date(), i = J(a), e = await E(["variant", a.id, i], y);
78
+ await n(
79
+ U(e),
80
+ (P) => ({
81
+ ...P,
82
+ variantId: i,
83
+ participationWindow: {
84
+ start: C(t, 2),
85
+ end: R(t, a.onlineSettings?.duration ?? 0)
86
+ }
87
+ })
88
+ );
89
+ }, [a, n]);
90
+ return /* @__PURE__ */ u(j, { title: d, children: /* @__PURE__ */ u(q.Provider, { value: { start: b }, children: /* @__PURE__ */ u(
91
+ D,
92
+ {
93
+ contest: a,
94
+ venue: S,
95
+ student: s,
96
+ setAnswer: g,
97
+ submit: h,
98
+ reset: W,
99
+ logout: T,
100
+ enforceFullscreen: !1,
101
+ schema: m?.schema,
102
+ children: c
103
+ }
104
+ ) }) });
105
+ }
106
+ B.displayName = "TrainingProvider";
107
+ function G(a) {
108
+ return {
109
+ id: "",
110
+ userData: {
111
+ name: "Utente",
112
+ surname: "anonimo"
113
+ },
114
+ answers: {},
115
+ contestId: a.id
116
+ };
117
+ }
118
+ const H = new k();
119
+ function J(a) {
120
+ return H.choice(a.variantIds);
121
+ }
122
+ async function y([, a, c]) {
123
+ const d = await fetch(`/variants/${a}/${c}/answers.json`);
124
+ if (!d.ok) throw new Error("Failed to fetch schema");
125
+ const o = await d.json();
126
+ return x(A, o);
127
+ }
128
+ export {
129
+ B as TrainingProvider
130
+ };
131
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sources":["../../src/entry/provider.tsx"],"sourcesContent":["import { type ReactNode, useCallback, useMemo, useState } from \"react\";\n\nimport { TitleProvider } from \"@olinfo/quizms/components\";\nimport {\n type Answer,\n type Contest,\n type Student,\n type Variant,\n type VariantsConfig,\n type Venue,\n variantSchema,\n} from \"@olinfo/quizms/models\";\nimport { StudentProvider } from \"@olinfo/quizms/student\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { addMinutes, subSeconds } from \"date-fns\";\nimport useSWR, { preload } from \"swr\";\n\nimport {\n getStudentIframe,\n getTitleIframe,\n logoutIframe,\n resetIframe,\n setAnswersIframe,\n startIframe,\n submitIframe,\n} from \"./iframe\";\nimport { TrainingStatementContext } from \"./statement\";\n\nexport function TrainingProvider({\n contest,\n children,\n}: {\n contest: Contest & VariantsConfig;\n children: ReactNode;\n}) {\n const { data: title } = useSWR(\"getTitle\", getTitleIframe, { suspense: true });\n\n const { data: iframeStudent, mutate } = useSWR([\"student\", contest.id], getStudentIframe, {\n suspense: true,\n });\n const [anonymousStudent, setAnonymousStudent] = useState<Student>(\n createAnonymousStudent(contest),\n );\n\n const { student, type } = useMemo(\n () =>\n iframeStudent\n ? { type: \"iframe\", student: iframeStudent }\n : { type: \"anonymous\", student: anonymousStudent },\n [iframeStudent, anonymousStudent],\n );\n\n const { data: variant } = useSWR(\n student.variantId && [\"variant\", contest.id, student.variantId],\n getVariant,\n {\n revalidateOnFocus: false,\n revalidateIfStale: false,\n },\n );\n\n const updateStudent = useCallback(\n async (\n iframeFn: (student: Student, contest: Contest) => Promise<Student | null>,\n updateFn: (student: Student) => Student,\n ) => {\n if (type === \"anonymous\") {\n setAnonymousStudent(updateFn);\n } else {\n await mutate((student) => (student ? iframeFn(updateFn(student), contest) : null), {\n optimisticData: (student) => (student ? updateFn(student) : null),\n });\n }\n },\n [type, contest, mutate],\n );\n\n const mockVenue: Venue = useMemo(\n () => ({\n id: \"\",\n schoolId: \"\",\n contestId: contest.id,\n name: \"\",\n participationWindow: student.participationWindow,\n finalized: false,\n disabled: false,\n pdfVariants: [],\n }),\n [contest.id, student.participationWindow],\n );\n\n const setAnswer = useCallback(\n async (problemId: string, answer: Answer) => {\n if (!variant) throw new Error(\"Variant non loaded\");\n await updateStudent(\n setAnswersIframe(variant),\n (student) =>\n ({ ...student, answers: { ...student.answers, [problemId]: answer } }) satisfies Student,\n );\n },\n [updateStudent, variant],\n );\n\n const submit = useCallback(async () => {\n await updateStudent(\n submitIframe,\n (student) =>\n ({\n ...student,\n participationWindow: { start: student.participationWindow!.start, end: new Date() },\n }) satisfies Student,\n );\n }, [updateStudent]);\n\n const reset = useCallback(async () => {\n await updateStudent(\n resetIframe,\n (student) =>\n ({\n ...student,\n answers: {},\n participationWindow: undefined,\n }) satisfies Student,\n );\n }, [updateStudent]);\n\n const logout = useCallback(async () => {\n await updateStudent(logoutIframe, (student) => student);\n }, [updateStudent]);\n\n const start = useCallback(async () => {\n const now = new Date();\n const variantId = getRandomVariant(contest);\n const variant = await preload([\"variant\", contest.id, variantId], getVariant);\n await updateStudent(\n startIframe(variant),\n (student) =>\n ({\n ...student,\n variantId,\n participationWindow: {\n start: subSeconds(now, 2),\n end: addMinutes(now, contest.onlineSettings?.duration ?? 0),\n },\n }) satisfies Student,\n );\n }, [contest, updateStudent]);\n\n return (\n <TitleProvider title={title}>\n <TrainingStatementContext.Provider value={{ start }}>\n <StudentProvider\n contest={contest}\n venue={mockVenue}\n student={student}\n setAnswer={setAnswer}\n submit={submit}\n reset={reset}\n logout={logout}\n enforceFullscreen={false}\n schema={variant?.schema}>\n {children}\n </StudentProvider>\n </TrainingStatementContext.Provider>\n </TitleProvider>\n );\n}\nTrainingProvider.displayName = \"TrainingProvider\";\n\nfunction createAnonymousStudent(contest: VariantsConfig): Student {\n return {\n id: \"\",\n userData: {\n name: \"Utente\",\n surname: \"anonimo\",\n },\n answers: {},\n contestId: contest.id,\n };\n}\n\nconst rng = new Rng();\n\nfunction getRandomVariant(contest: VariantsConfig): string {\n return rng.choice(contest.variantIds);\n}\n\nasync function getVariant([, contestId, variantId]: [string, string, string]): Promise<Variant> {\n const resp = await fetch(`/variants/${contestId}/${variantId}/answers.json`);\n if (!resp.ok) throw new Error(\"Failed to fetch schema\");\n const json: unknown = await resp.json();\n return validate(variantSchema, json);\n}\n"],"names":["TrainingProvider","contest","children","title","useSWR","getTitleIframe","iframeStudent","mutate","getStudentIframe","anonymousStudent","setAnonymousStudent","useState","createAnonymousStudent","student","type","useMemo","variant","getVariant","updateStudent","useCallback","iframeFn","updateFn","mockVenue","setAnswer","problemId","answer","setAnswersIframe","submit","submitIframe","reset","resetIframe","logout","logoutIframe","start","now","variantId","getRandomVariant","preload","startIframe","subSeconds","addMinutes","jsx","TitleProvider","TrainingStatementContext","StudentProvider","rng","Rng","contestId","resp","json","validate","variantSchema"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { createContext as a, useContext as o } from "react";
3
+ import { useStudent as s, RemoteStatement as m } from "@olinfo/quizms/student";
4
+ import { Button as c } from "@olinfo/react-components";
5
+ const l = a({});
6
+ function p() {
7
+ const { student: t, contest: n } = s(), { start: i } = o(l);
8
+ return t.participationWindow ? /* @__PURE__ */ e(
9
+ m,
10
+ {
11
+ statementUrl: () => `/variants/${n.id}/${t.variantId}/statement.txt`,
12
+ moduleUrl: (r) => `/variants/${n.id}/${t.variantId}/${r}`
13
+ }
14
+ ) : /* @__PURE__ */ e("div", { className: "flex h-[50vh] flex-col items-center justify-center", children: /* @__PURE__ */ e(c, { className: "btn-success btn-lg", onClick: i, children: "Inizia" }) });
15
+ }
16
+ export {
17
+ p as TrainingStatement,
18
+ l as TrainingStatementContext
19
+ };
20
+ //# sourceMappingURL=statement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statement.js","sources":["../../src/entry/statement.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\n\nimport { RemoteStatement, useStudent } from \"@olinfo/quizms/student\";\nimport { Button } from \"@olinfo/react-components\";\n\nexport const TrainingStatementContext = createContext<{\n start?: () => Promise<void> | void;\n}>({});\n\nexport function TrainingStatement() {\n const { student, contest } = useStudent();\n const { start } = useContext(TrainingStatementContext);\n\n if (!student.participationWindow) {\n return (\n <div className=\"flex h-[50vh] flex-col items-center justify-center\">\n <Button className=\"btn-success btn-lg\" onClick={start}>\n Inizia\n </Button>\n </div>\n );\n }\n\n return (\n <RemoteStatement\n statementUrl={() => `/variants/${contest.id}/${student.variantId}/statement.txt`}\n moduleUrl={(id) => `/variants/${contest.id}/${student.variantId}/${id}`}\n />\n );\n}\n"],"names":["TrainingStatementContext","createContext","TrainingStatement","student","contest","useStudent","start","useContext","jsx","RemoteStatement","id","Button"],"mappings":";;;;AAKO,MAAMA,IAA2BC,EAErC,CAAA,CAAE;AAEE,SAASC,IAAoB;AAClC,QAAM,EAAE,SAAAC,GAAS,SAAAC,EAAA,IAAYC,EAAA,GACvB,EAAE,OAAAC,EAAA,IAAUC,EAAWP,CAAwB;AAErD,SAAKG,EAAQ,sBAWX,gBAAAK;AAAA,IAACC;AAAA,IAAA;AAAA,MACC,cAAc,MAAM,aAAaL,EAAQ,EAAE,IAAID,EAAQ,SAAS;AAAA,MAChE,WAAW,CAACO,MAAO,aAAaN,EAAQ,EAAE,IAAID,EAAQ,SAAS,IAAIO,CAAE;AAAA,IAAA;AAAA,EAAA,IAXrE,gBAAAF,EAAC,OAAA,EAAI,WAAU,sDACb,UAAA,gBAAAA,EAACG,GAAA,EAAO,WAAU,sBAAqB,SAASL,GAAO,UAAA,SAAA,CAEvD,GACF;AAUN;"}
@@ -0,0 +1,22 @@
1
+ import { Contest } from '@olinfo/quizms/models';
2
+ import { Context } from 'react';
3
+ import { JSX } from 'react/jsx-runtime';
4
+ import { ReactNode } from 'react';
5
+ import { VariantsConfig } from '@olinfo/quizms/models';
6
+
7
+ export declare function TrainingProvider({ contest, children, }: {
8
+ contest: Contest & VariantsConfig;
9
+ children: ReactNode;
10
+ }): JSX.Element;
11
+
12
+ export declare namespace TrainingProvider {
13
+ var displayName: string;
14
+ }
15
+
16
+ export declare function TrainingStatement(): JSX.Element;
17
+
18
+ export declare const TrainingStatementContext: Context< {
19
+ start?: () => Promise<void> | void;
20
+ }>;
21
+
22
+ export { }
package/dist/entry.js ADDED
@@ -0,0 +1,8 @@
1
+ import { TrainingProvider as r } from "./entry/provider.js";
2
+ import { TrainingStatement as i, TrainingStatementContext as o } from "./entry/statement.js";
3
+ export {
4
+ r as TrainingProvider,
5
+ i as TrainingStatement,
6
+ o as TrainingStatementContext
7
+ };
8
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,43 @@
1
+ import { reactComponentCase as n } from "@olinfo/quizms/utils";
2
+ import { loadContests as u } from "@olinfo/quizms/utils-node";
3
+ import { transformWithEsbuild as p } from "vite";
4
+ async function y() {
5
+ const o = await u();
6
+ return {
7
+ name: "quizms:training-entry",
8
+ resolveId(r) {
9
+ const [i] = r.split("?");
10
+ if (i === "virtual:quizms-training-entry")
11
+ return `\0${r}`;
12
+ },
13
+ load(r) {
14
+ const [i, s] = r.split("?");
15
+ if (i === "\0virtual:quizms-training-entry") {
16
+ const e = new URLSearchParams(s), m = e.get("id"), t = o.find((d) => d.id === m), a = e.has("dev"), c = `
17
+ import { createApp } from "@olinfo/quizms/entry";
18
+ import { TrainingProvider, TrainingStatement } from "@olinfo/quizms-training/entry";
19
+ import { Route, Switch } from "wouter";
20
+
21
+ ${a ? `import ${n(t.id)} from "/${t.entry}";` : ""}
22
+ ${t.header ? `import ${n(t.id)}Header from "/${t.header}";` : ""}
23
+
24
+ export default function createTrainingEntry() {
25
+ return createApp(
26
+ <TrainingProvider contest={${JSON.stringify(t)}}>
27
+ ${t.header ? `<${n(t.id)}Header />` : ""}
28
+ ${a ? `<${n(t.id)} />` : "<TrainingStatement />"}
29
+ </TrainingProvider>
30
+ );
31
+ }
32
+ `;
33
+ return p(c, "virtual-quizms-training-entry.jsx", {
34
+ jsx: "automatic"
35
+ });
36
+ }
37
+ }
38
+ };
39
+ }
40
+ export {
41
+ y as default
42
+ };
43
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sources":["../../src/vite/entry.ts"],"sourcesContent":["import { reactComponentCase } from \"@olinfo/quizms/utils\";\nimport { loadContests } from \"@olinfo/quizms/utils-node\";\nimport { type PluginOption, transformWithEsbuild } from \"vite\";\n\nexport default async function trainingEntry(): Promise<PluginOption> {\n const contests = await loadContests();\n\n return {\n name: \"quizms:training-entry\",\n resolveId(id) {\n const [pathname] = id.split(\"?\");\n if (pathname === \"virtual:quizms-training-entry\") {\n return `\\0${id}`;\n }\n },\n load(id) {\n const [pathname, query] = id.split(\"?\");\n if (pathname === \"\\0virtual:quizms-training-entry\") {\n const params = new URLSearchParams(query);\n const contestId = params.get(\"id\");\n const contest = contests.find((c) => c.id === contestId)!;\n const isDev = params.has(\"dev\");\n\n const entry = `\nimport { createApp } from \"@olinfo/quizms/entry\";\nimport { TrainingProvider, TrainingStatement } from \"@olinfo/quizms-training/entry\";\nimport { Route, Switch } from \"wouter\";\n\n${isDev ? `import ${reactComponentCase(contest.id)} from \"/${contest.entry}\";` : \"\"}\n${contest.header ? `import ${reactComponentCase(contest.id)}Header from \"/${contest.header}\";` : \"\"}\n\nexport default function createTrainingEntry() {\n return createApp(\n <TrainingProvider contest={${JSON.stringify(contest)}}>\n ${contest.header ? `<${reactComponentCase(contest.id)}Header />` : \"\"}\n ${isDev ? `<${reactComponentCase(contest.id)} />` : \"<TrainingStatement />\"}\n </TrainingProvider>\n );\n}\n`;\n return transformWithEsbuild(entry, \"virtual-quizms-training-entry.jsx\", {\n jsx: \"automatic\",\n });\n }\n },\n };\n}\n"],"names":["trainingEntry","contests","loadContests","id","pathname","query","params","contestId","contest","c","isDev","entry","reactComponentCase","transformWithEsbuild"],"mappings":";;;AAIA,eAA8BA,IAAuC;AACnE,QAAMC,IAAW,MAAMC,EAAA;AAEvB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAUC,GAAI;AACZ,YAAM,CAACC,CAAQ,IAAID,EAAG,MAAM,GAAG;AAC/B,UAAIC,MAAa;AACf,eAAO,KAAKD,CAAE;AAAA,IAElB;AAAA,IACA,KAAKA,GAAI;AACP,YAAM,CAACC,GAAUC,CAAK,IAAIF,EAAG,MAAM,GAAG;AACtC,UAAIC,MAAa,mCAAmC;AAClD,cAAME,IAAS,IAAI,gBAAgBD,CAAK,GAClCE,IAAYD,EAAO,IAAI,IAAI,GAC3BE,IAAUP,EAAS,KAAK,CAACQ,MAAMA,EAAE,OAAOF,CAAS,GACjDG,IAAQJ,EAAO,IAAI,KAAK,GAExBK,IAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpBD,IAAQ,UAAUE,EAAmBJ,EAAQ,EAAE,CAAC,WAAWA,EAAQ,KAAK,OAAO,EAAE;AAAA,EACjFA,EAAQ,SAAS,UAAUI,EAAmBJ,EAAQ,EAAE,CAAC,iBAAiBA,EAAQ,MAAM,OAAO,EAAE;AAAA;AAAA;AAAA;AAAA,iCAIlE,KAAK,UAAUA,CAAO,CAAC;AAAA,QAChDA,EAAQ,SAAS,IAAII,EAAmBJ,EAAQ,EAAE,CAAC,cAAc,EAAE;AAAA,QACnEE,IAAQ,IAAIE,EAAmBJ,EAAQ,EAAE,CAAC,QAAQ,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAKzE,eAAOK,EAAqBF,GAAO,qCAAqC;AAAA,UACtE,KAAK;AAAA,QAAA,CACN;AAAA,MACH;AAAA,IACF;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,44 @@
1
+ import { loadContests as n } from "@olinfo/quizms/utils-node";
2
+ import { defaultClientMainFields as e, defaultClientConditions as r } from "vite";
3
+ async function a() {
4
+ const i = await n();
5
+ return {
6
+ name: "quizms:training-preset",
7
+ apply: "build",
8
+ config() {
9
+ return {
10
+ environments: {
11
+ training: {
12
+ resolve: {
13
+ conditions: [...r],
14
+ mainFields: [...e],
15
+ noExternal: !0
16
+ },
17
+ build: {
18
+ rollupOptions: {
19
+ input: Object.fromEntries(
20
+ i.map((t) => [
21
+ t.id,
22
+ `virtual:quizms-entry?id=${encodeURIComponent(`virtual:quizms-training-entry?id=${t.id}`)}`
23
+ ])
24
+ )
25
+ },
26
+ sourcemap: !0
27
+ }
28
+ }
29
+ }
30
+ };
31
+ },
32
+ buildStart() {
33
+ this.environment.name === "training" && (process.env.QUIZMS_SHOW_SOLUTIONS = "true", this.emitFile({
34
+ type: "asset",
35
+ fileName: "contests.json",
36
+ source: JSON.stringify(i)
37
+ }));
38
+ }
39
+ };
40
+ }
41
+ export {
42
+ a as default
43
+ };
44
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.js","sources":["../../src/vite/preset.ts"],"sourcesContent":["import { loadContests } from \"@olinfo/quizms/utils-node\";\nimport { defaultClientConditions, defaultClientMainFields, type PluginOption } from \"vite\";\n\nexport default async function trainingPreset(): Promise<PluginOption> {\n const contests = await loadContests();\n\n return {\n name: \"quizms:training-preset\",\n apply: \"build\",\n config() {\n return {\n environments: {\n training: {\n resolve: {\n conditions: [...defaultClientConditions],\n mainFields: [...defaultClientMainFields],\n noExternal: true,\n },\n build: {\n rollupOptions: {\n input: Object.fromEntries(\n contests.map((contest) => [\n contest.id,\n `virtual:quizms-entry?id=${encodeURIComponent(`virtual:quizms-training-entry?id=${contest.id}`)}`,\n ]),\n ),\n },\n sourcemap: true,\n },\n },\n },\n };\n },\n buildStart() {\n if (this.environment.name === \"training\") {\n process.env.QUIZMS_SHOW_SOLUTIONS = \"true\";\n this.emitFile({\n type: \"asset\",\n fileName: \"contests.json\",\n source: JSON.stringify(contests),\n });\n }\n },\n };\n}\n"],"names":["trainingPreset","contests","loadContests","defaultClientConditions","defaultClientMainFields","contest"],"mappings":";;AAGA,eAA8BA,IAAwC;AACpE,QAAMC,IAAW,MAAMC,EAAA;AAEvB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AACP,aAAO;AAAA,QACL,cAAc;AAAA,UACZ,UAAU;AAAA,YACR,SAAS;AAAA,cACP,YAAY,CAAC,GAAGC,CAAuB;AAAA,cACvC,YAAY,CAAC,GAAGC,CAAuB;AAAA,cACvC,YAAY;AAAA,YAAA;AAAA,YAEd,OAAO;AAAA,cACL,eAAe;AAAA,gBACb,OAAO,OAAO;AAAA,kBACZH,EAAS,IAAI,CAACI,MAAY;AAAA,oBACxBA,EAAQ;AAAA,oBACR,2BAA2B,mBAAmB,oCAAoCA,EAAQ,EAAE,EAAE,CAAC;AAAA,kBAAA,CAChG;AAAA,gBAAA;AAAA,cACH;AAAA,cAEF,WAAW;AAAA,YAAA;AAAA,UACb;AAAA,QACF;AAAA,MACF;AAAA,IAEJ;AAAA,IACA,aAAa;AACX,MAAI,KAAK,YAAY,SAAS,eAC5B,QAAQ,IAAI,wBAAwB,QACpC,KAAK,SAAS;AAAA,QACZ,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ,KAAK,UAAUJ,CAAQ;AAAA,MAAA,CAChC;AAAA,IAEL;AAAA,EAAA;AAEJ;"}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare const _default: any;
2
+ export default _default;
3
+
4
+ export { }
package/dist/vite.js ADDED
@@ -0,0 +1,7 @@
1
+ import r from "./vite/entry.js";
2
+ import t from "./vite/preset.js";
3
+ const o = [r(), t()];
4
+ export {
5
+ o as default
6
+ };
7
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","sources":["../src/vite/index.ts"],"sourcesContent":["import trainingEntry from \"./entry\";\nimport trainingPreset from \"./preset\";\n\nexport default [trainingEntry(), trainingPreset()] as any;\n"],"names":["index","trainingEntry","trainingPreset"],"mappings":";;AAGA,MAAAA,IAAe,CAACC,EAAA,GAAiBC,GAAgB;"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@olinfo/quizms-training",
3
+ "version": "5.0.0",
4
+ "type": "module",
5
+ "author": "Alessandro Bortolin <bortolin.alessandro@outlook.it>",
6
+ "license": "MIT",
7
+ "exports": {
8
+ "./entry": {
9
+ "types": "./dist/entry.d.ts",
10
+ "default": "./dist/entry.js"
11
+ },
12
+ "./vite": {
13
+ "types": "./dist/vite.d.ts",
14
+ "default": "./dist/vite.js"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "@olinfo/react-components": "^0.12.2",
19
+ "@types/lodash-es": "^4.17.12",
20
+ "date-fns": "^4.1.0",
21
+ "lodash-es": "^4.17.23",
22
+ "swr": "^2.4.1",
23
+ "vite": "^7.3.1",
24
+ "zod": "^4.3.6",
25
+ "@olinfo/quizms": "^5.0.2"
26
+ },
27
+ "peerDependencies": {
28
+ "react": "^19.2.4"
29
+ },
30
+ "postcss": {
31
+ "plugins": {
32
+ "tailwindcss": {}
33
+ }
34
+ },
35
+ "devDependencies": {
36
+ "@biomejs/biome": "^2.4.6",
37
+ "@types/node": "^25.3.5",
38
+ "@types/react": "^19.2.14",
39
+ "rollup-plugin-node-externals": "^8.1.2",
40
+ "rollup-preserve-directives": "^1.1.3",
41
+ "typescript": "5.8.2",
42
+ "vite-plugin-dts": "^4.5.4",
43
+ "vite-tsconfig-paths": "^6.1.1"
44
+ },
45
+ "scripts": {
46
+ "prebuild": "biome check && tsc --noEmit",
47
+ "build": "vite build",
48
+ "watch": "vite build --watch",
49
+ "lint": "biome check --write && tsc --noEmit"
50
+ }
51
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ type Contest,
3
+ calcScore,
4
+ type Student,
5
+ studentSchema,
6
+ type Variant,
7
+ } from "@olinfo/quizms/models";
8
+ import { validate } from "@olinfo/quizms/utils";
9
+ import { cloneDeepWith, isDate, sumBy } from "lodash-es";
10
+ import z from "zod";
11
+
12
+ export async function getTitleIframe(): Promise<string> {
13
+ if (window.parent === window) return "Quizms";
14
+
15
+ for (let i = 0; i < 50; i++) {
16
+ try {
17
+ return await getResponse({ type: "getTitle" }, z.string(), 200);
18
+ } catch {}
19
+ }
20
+ throw new Error("Failed to connect");
21
+ }
22
+
23
+ export async function getStudentIframe([_, contestId]: [string, string]): Promise<Student | null> {
24
+ if (window.parent === window) return null;
25
+ return await getResponse({ type: "getStudent", contestId }, studentSchema.nullable());
26
+ }
27
+
28
+ export function setAnswersIframe(variant: Variant) {
29
+ return async (student: Student, contest: Contest): Promise<Student | null> => {
30
+ if (window.parent === window) return null;
31
+
32
+ return await getResponse(
33
+ {
34
+ type: "setAnswers",
35
+ contestId: contest.id,
36
+ answers: student.answers,
37
+ score: calcScore(student, variant.schema),
38
+ maxScore: sumBy(Object.values(variant.schema).map((problem) => problem.pointsCorrect)),
39
+ },
40
+ studentSchema,
41
+ );
42
+ };
43
+ }
44
+
45
+ export function startIframe(variant: Variant) {
46
+ return async (student: Student, contest: Contest): Promise<Student | null> => {
47
+ if (window.parent === window) return null;
48
+
49
+ return await getResponse(
50
+ {
51
+ type: "start",
52
+ contestId: contest.id,
53
+ variantId: student.variantId,
54
+ duration: contest.onlineSettings?.duration ?? 0,
55
+ score: calcScore(student, variant.schema),
56
+ maxScore: sumBy(Object.values(variant.schema).map((problem) => problem.pointsCorrect)),
57
+ },
58
+ studentSchema,
59
+ );
60
+ };
61
+ }
62
+
63
+ export async function submitIframe(_student: Student, contest: Contest): Promise<Student | null> {
64
+ if (window.parent === window) return null;
65
+ return await getResponse({ type: "submit", contestId: contest.id }, studentSchema);
66
+ }
67
+
68
+ export async function resetIframe(_student: Student, contest: Contest): Promise<Student | null> {
69
+ if (window.parent === window) return null;
70
+ return await getResponse({ type: "reset", contestId: contest.id }, studentSchema);
71
+ }
72
+
73
+ export async function logoutIframe(): Promise<null> {
74
+ if (window.parent === window) return null;
75
+ return await getResponse({ type: "logout" }, z.null());
76
+ }
77
+
78
+ async function getResponse<Message extends { type: string }, Data>(
79
+ message: Message,
80
+ schema: z.ZodType<Data>,
81
+ timeout = 10_000,
82
+ ): Promise<Data> {
83
+ const messageId = crypto.randomUUID();
84
+
85
+ const controller = new AbortController();
86
+ const responsePromise = new Promise<Data>((resolve, reject) => {
87
+ window.addEventListener(
88
+ "message",
89
+ (ev) => {
90
+ try {
91
+ const response = validate(
92
+ messageSchema,
93
+ cloneDeepWith(ev.data, (value) => (isDate(value) ? new Date(value) : undefined)),
94
+ );
95
+ if (response.messageId !== messageId) return;
96
+
97
+ if (response.success) {
98
+ resolve(validate(schema, response.data));
99
+ } else {
100
+ reject(new Error(response.error));
101
+ }
102
+ } catch (err) {
103
+ reject(err);
104
+ }
105
+ },
106
+ { signal: controller.signal },
107
+ );
108
+ });
109
+
110
+ window.parent.postMessage({ ...message, messageId }, "*");
111
+
112
+ try {
113
+ const timeoutPromise = new Promise<never>((_, reject) => setTimeout(() => reject(), timeout));
114
+ return await Promise.race([responsePromise, timeoutPromise]);
115
+ } finally {
116
+ controller.abort();
117
+ }
118
+ }
119
+
120
+ const messageSchema = z.discriminatedUnion("success", [
121
+ z.object({
122
+ success: z.literal(true),
123
+ messageId: z.string(),
124
+ data: z.any(),
125
+ }),
126
+ z.object({
127
+ success: z.literal(false),
128
+ messageId: z.string(),
129
+ error: z.string(),
130
+ }),
131
+ ]);
@@ -0,0 +1,2 @@
1
+ export * from "./provider";
2
+ export * from "./statement";
@@ -0,0 +1,193 @@
1
+ import { type ReactNode, useCallback, useMemo, useState } from "react";
2
+
3
+ import { TitleProvider } from "@olinfo/quizms/components";
4
+ import {
5
+ type Answer,
6
+ type Contest,
7
+ type Student,
8
+ type Variant,
9
+ type VariantsConfig,
10
+ type Venue,
11
+ variantSchema,
12
+ } from "@olinfo/quizms/models";
13
+ import { StudentProvider } from "@olinfo/quizms/student";
14
+ import { Rng, validate } from "@olinfo/quizms/utils";
15
+ import { addMinutes, subSeconds } from "date-fns";
16
+ import useSWR, { preload } from "swr";
17
+
18
+ import {
19
+ getStudentIframe,
20
+ getTitleIframe,
21
+ logoutIframe,
22
+ resetIframe,
23
+ setAnswersIframe,
24
+ startIframe,
25
+ submitIframe,
26
+ } from "./iframe";
27
+ import { TrainingStatementContext } from "./statement";
28
+
29
+ export function TrainingProvider({
30
+ contest,
31
+ children,
32
+ }: {
33
+ contest: Contest & VariantsConfig;
34
+ children: ReactNode;
35
+ }) {
36
+ const { data: title } = useSWR("getTitle", getTitleIframe, { suspense: true });
37
+
38
+ const { data: iframeStudent, mutate } = useSWR(["student", contest.id], getStudentIframe, {
39
+ suspense: true,
40
+ });
41
+ const [anonymousStudent, setAnonymousStudent] = useState<Student>(
42
+ createAnonymousStudent(contest),
43
+ );
44
+
45
+ const { student, type } = useMemo(
46
+ () =>
47
+ iframeStudent
48
+ ? { type: "iframe", student: iframeStudent }
49
+ : { type: "anonymous", student: anonymousStudent },
50
+ [iframeStudent, anonymousStudent],
51
+ );
52
+
53
+ const { data: variant } = useSWR(
54
+ student.variantId && ["variant", contest.id, student.variantId],
55
+ getVariant,
56
+ {
57
+ revalidateOnFocus: false,
58
+ revalidateIfStale: false,
59
+ },
60
+ );
61
+
62
+ const updateStudent = useCallback(
63
+ async (
64
+ iframeFn: (student: Student, contest: Contest) => Promise<Student | null>,
65
+ updateFn: (student: Student) => Student,
66
+ ) => {
67
+ if (type === "anonymous") {
68
+ setAnonymousStudent(updateFn);
69
+ } else {
70
+ await mutate((student) => (student ? iframeFn(updateFn(student), contest) : null), {
71
+ optimisticData: (student) => (student ? updateFn(student) : null),
72
+ });
73
+ }
74
+ },
75
+ [type, contest, mutate],
76
+ );
77
+
78
+ const mockVenue: Venue = useMemo(
79
+ () => ({
80
+ id: "",
81
+ schoolId: "",
82
+ contestId: contest.id,
83
+ name: "",
84
+ participationWindow: student.participationWindow,
85
+ finalized: false,
86
+ disabled: false,
87
+ pdfVariants: [],
88
+ }),
89
+ [contest.id, student.participationWindow],
90
+ );
91
+
92
+ const setAnswer = useCallback(
93
+ async (problemId: string, answer: Answer) => {
94
+ if (!variant) throw new Error("Variant non loaded");
95
+ await updateStudent(
96
+ setAnswersIframe(variant),
97
+ (student) =>
98
+ ({ ...student, answers: { ...student.answers, [problemId]: answer } }) satisfies Student,
99
+ );
100
+ },
101
+ [updateStudent, variant],
102
+ );
103
+
104
+ const submit = useCallback(async () => {
105
+ await updateStudent(
106
+ submitIframe,
107
+ (student) =>
108
+ ({
109
+ ...student,
110
+ participationWindow: { start: student.participationWindow!.start, end: new Date() },
111
+ }) satisfies Student,
112
+ );
113
+ }, [updateStudent]);
114
+
115
+ const reset = useCallback(async () => {
116
+ await updateStudent(
117
+ resetIframe,
118
+ (student) =>
119
+ ({
120
+ ...student,
121
+ answers: {},
122
+ participationWindow: undefined,
123
+ }) satisfies Student,
124
+ );
125
+ }, [updateStudent]);
126
+
127
+ const logout = useCallback(async () => {
128
+ await updateStudent(logoutIframe, (student) => student);
129
+ }, [updateStudent]);
130
+
131
+ const start = useCallback(async () => {
132
+ const now = new Date();
133
+ const variantId = getRandomVariant(contest);
134
+ const variant = await preload(["variant", contest.id, variantId], getVariant);
135
+ await updateStudent(
136
+ startIframe(variant),
137
+ (student) =>
138
+ ({
139
+ ...student,
140
+ variantId,
141
+ participationWindow: {
142
+ start: subSeconds(now, 2),
143
+ end: addMinutes(now, contest.onlineSettings?.duration ?? 0),
144
+ },
145
+ }) satisfies Student,
146
+ );
147
+ }, [contest, updateStudent]);
148
+
149
+ return (
150
+ <TitleProvider title={title}>
151
+ <TrainingStatementContext.Provider value={{ start }}>
152
+ <StudentProvider
153
+ contest={contest}
154
+ venue={mockVenue}
155
+ student={student}
156
+ setAnswer={setAnswer}
157
+ submit={submit}
158
+ reset={reset}
159
+ logout={logout}
160
+ enforceFullscreen={false}
161
+ schema={variant?.schema}>
162
+ {children}
163
+ </StudentProvider>
164
+ </TrainingStatementContext.Provider>
165
+ </TitleProvider>
166
+ );
167
+ }
168
+ TrainingProvider.displayName = "TrainingProvider";
169
+
170
+ function createAnonymousStudent(contest: VariantsConfig): Student {
171
+ return {
172
+ id: "",
173
+ userData: {
174
+ name: "Utente",
175
+ surname: "anonimo",
176
+ },
177
+ answers: {},
178
+ contestId: contest.id,
179
+ };
180
+ }
181
+
182
+ const rng = new Rng();
183
+
184
+ function getRandomVariant(contest: VariantsConfig): string {
185
+ return rng.choice(contest.variantIds);
186
+ }
187
+
188
+ async function getVariant([, contestId, variantId]: [string, string, string]): Promise<Variant> {
189
+ const resp = await fetch(`/variants/${contestId}/${variantId}/answers.json`);
190
+ if (!resp.ok) throw new Error("Failed to fetch schema");
191
+ const json: unknown = await resp.json();
192
+ return validate(variantSchema, json);
193
+ }
@@ -0,0 +1,30 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ import { RemoteStatement, useStudent } from "@olinfo/quizms/student";
4
+ import { Button } from "@olinfo/react-components";
5
+
6
+ export const TrainingStatementContext = createContext<{
7
+ start?: () => Promise<void> | void;
8
+ }>({});
9
+
10
+ export function TrainingStatement() {
11
+ const { student, contest } = useStudent();
12
+ const { start } = useContext(TrainingStatementContext);
13
+
14
+ if (!student.participationWindow) {
15
+ return (
16
+ <div className="flex h-[50vh] flex-col items-center justify-center">
17
+ <Button className="btn-success btn-lg" onClick={start}>
18
+ Inizia
19
+ </Button>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <RemoteStatement
26
+ statementUrl={() => `/variants/${contest.id}/${student.variantId}/statement.txt`}
27
+ moduleUrl={(id) => `/variants/${contest.id}/${student.variantId}/${id}`}
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,47 @@
1
+ import { reactComponentCase } from "@olinfo/quizms/utils";
2
+ import { loadContests } from "@olinfo/quizms/utils-node";
3
+ import { type PluginOption, transformWithEsbuild } from "vite";
4
+
5
+ export default async function trainingEntry(): Promise<PluginOption> {
6
+ const contests = await loadContests();
7
+
8
+ return {
9
+ name: "quizms:training-entry",
10
+ resolveId(id) {
11
+ const [pathname] = id.split("?");
12
+ if (pathname === "virtual:quizms-training-entry") {
13
+ return `\0${id}`;
14
+ }
15
+ },
16
+ load(id) {
17
+ const [pathname, query] = id.split("?");
18
+ if (pathname === "\0virtual:quizms-training-entry") {
19
+ const params = new URLSearchParams(query);
20
+ const contestId = params.get("id");
21
+ const contest = contests.find((c) => c.id === contestId)!;
22
+ const isDev = params.has("dev");
23
+
24
+ const entry = `
25
+ import { createApp } from "@olinfo/quizms/entry";
26
+ import { TrainingProvider, TrainingStatement } from "@olinfo/quizms-training/entry";
27
+ import { Route, Switch } from "wouter";
28
+
29
+ ${isDev ? `import ${reactComponentCase(contest.id)} from "/${contest.entry}";` : ""}
30
+ ${contest.header ? `import ${reactComponentCase(contest.id)}Header from "/${contest.header}";` : ""}
31
+
32
+ export default function createTrainingEntry() {
33
+ return createApp(
34
+ <TrainingProvider contest={${JSON.stringify(contest)}}>
35
+ ${contest.header ? `<${reactComponentCase(contest.id)}Header />` : ""}
36
+ ${isDev ? `<${reactComponentCase(contest.id)} />` : "<TrainingStatement />"}
37
+ </TrainingProvider>
38
+ );
39
+ }
40
+ `;
41
+ return transformWithEsbuild(entry, "virtual-quizms-training-entry.jsx", {
42
+ jsx: "automatic",
43
+ });
44
+ }
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,4 @@
1
+ import trainingEntry from "./entry";
2
+ import trainingPreset from "./preset";
3
+
4
+ export default [trainingEntry(), trainingPreset()] as any;
@@ -0,0 +1,45 @@
1
+ import { loadContests } from "@olinfo/quizms/utils-node";
2
+ import { defaultClientConditions, defaultClientMainFields, type PluginOption } from "vite";
3
+
4
+ export default async function trainingPreset(): Promise<PluginOption> {
5
+ const contests = await loadContests();
6
+
7
+ return {
8
+ name: "quizms:training-preset",
9
+ apply: "build",
10
+ config() {
11
+ return {
12
+ environments: {
13
+ training: {
14
+ resolve: {
15
+ conditions: [...defaultClientConditions],
16
+ mainFields: [...defaultClientMainFields],
17
+ noExternal: true,
18
+ },
19
+ build: {
20
+ rollupOptions: {
21
+ input: Object.fromEntries(
22
+ contests.map((contest) => [
23
+ contest.id,
24
+ `virtual:quizms-entry?id=${encodeURIComponent(`virtual:quizms-training-entry?id=${contest.id}`)}`,
25
+ ]),
26
+ ),
27
+ },
28
+ sourcemap: true,
29
+ },
30
+ },
31
+ },
32
+ };
33
+ },
34
+ buildStart() {
35
+ if (this.environment.name === "training") {
36
+ process.env.QUIZMS_SHOW_SOLUTIONS = "true";
37
+ this.emitFile({
38
+ type: "asset",
39
+ fileName: "contests.json",
40
+ source: JSON.stringify(contests),
41
+ });
42
+ }
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,7 @@
1
+ import olinfoPresets from "@olinfo/tailwind";
2
+
3
+ /** @type {import("tailwindcss").Config} */
4
+ export default {
5
+ presets: [olinfoPresets],
6
+ content: ["./src/**/*.{ts,tsx}", "./node_modules/@olinfo/react-components/dist/**/*.js"],
7
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "esnext",
4
+ "moduleResolution": "bundler",
5
+ "target": "es2022",
6
+ "declaration": true,
7
+ "declarationDir": "dist",
8
+ "jsx": "react-jsx",
9
+ "types": ["vite/client"],
10
+ "strict": true,
11
+ "noImplicitAny": true,
12
+ "allowJs": true,
13
+ "skipLibCheck": true,
14
+ "esModuleInterop": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "resolveJsonModule": true,
19
+ "isolatedModules": true,
20
+ "baseUrl": "./",
21
+ "paths": {
22
+ "~/*": ["./src/*"]
23
+ },
24
+ "rootDir": "src",
25
+ "outDir": "dist"
26
+ },
27
+ "include": ["src/"]
28
+ }
@@ -0,0 +1,26 @@
1
+ import nodeExternals from "rollup-plugin-node-externals";
2
+ import preserveDirectives from "rollup-preserve-directives";
3
+ import { defineConfig } from "vite";
4
+ import dts from "vite-plugin-dts";
5
+ import tsconfigPaths from "vite-tsconfig-paths";
6
+
7
+ export default defineConfig({
8
+ build: {
9
+ lib: {
10
+ entry: {
11
+ entry: "src/entry/index.tsx",
12
+ vite: "src/vite/index.ts",
13
+ },
14
+ formats: ["es"],
15
+ },
16
+ target: "esnext",
17
+ sourcemap: true,
18
+ rollupOptions: {
19
+ output: {
20
+ preserveModules: true,
21
+ hoistTransitiveImports: false,
22
+ },
23
+ },
24
+ },
25
+ plugins: [dts({ rollupTypes: true }), nodeExternals(), preserveDirectives(), tsconfigPaths()],
26
+ });