@olinfo/quizms-firebase 4.0.0 → 4.1.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.
Files changed (65) hide show
  1. package/dist/functions.d.ts +2 -11
  2. package/dist/functions.js +2 -2
  3. package/dist/package.json.js +1 -1
  4. package/dist/src/cli/files/functions-index.js +4 -4
  5. package/dist/src/cli/files/functions-index.js.map +1 -1
  6. package/dist/src/cli/init.js +8 -8
  7. package/dist/src/cli/init.js.map +1 -1
  8. package/dist/src/cli/update-scores.js +21 -32
  9. package/dist/src/cli/update-scores.js.map +1 -1
  10. package/dist/src/cli/utils/collection.js +20 -18
  11. package/dist/src/cli/utils/collection.js.map +1 -1
  12. package/dist/src/cli/utils/converters-admin.js +31 -32
  13. package/dist/src/cli/utils/converters-admin.js.map +1 -1
  14. package/dist/src/cli/utils/region-finder.js +6 -5
  15. package/dist/src/cli/utils/region-finder.js.map +1 -1
  16. package/dist/src/cli/utils/storage.js +26 -35
  17. package/dist/src/cli/utils/storage.js.map +1 -1
  18. package/dist/src/cli/utils/users.js +13 -12
  19. package/dist/src/cli/utils/users.js.map +1 -1
  20. package/dist/src/functions/api/admin.js +28 -0
  21. package/dist/src/functions/api/admin.js.map +1 -0
  22. package/dist/src/functions/api/index.js +26 -34
  23. package/dist/src/functions/api/index.js.map +1 -1
  24. package/dist/src/functions/api/queries.js +5 -5
  25. package/dist/src/functions/api/student.js +62 -72
  26. package/dist/src/functions/api/student.js.map +1 -1
  27. package/dist/src/functions/api/teacher.js +104 -111
  28. package/dist/src/functions/api/teacher.js.map +1 -1
  29. package/dist/src/functions/api/trpc.js +18 -0
  30. package/dist/src/functions/api/trpc.js.map +1 -0
  31. package/dist/src/functions/submissions.js +1 -1
  32. package/dist/src/functions/submissions.js.map +1 -1
  33. package/dist/src/models/user.js +2 -2
  34. package/dist/src/models/user.js.map +1 -1
  35. package/dist/src/models/website.js +1 -1
  36. package/dist/src/models/website.js.map +1 -1
  37. package/dist/src/vite/preset.js +3 -4
  38. package/dist/src/vite/preset.js.map +1 -1
  39. package/dist/src/web/admin/announcements.js +9 -10
  40. package/dist/src/web/admin/announcements.js.map +1 -1
  41. package/dist/src/web/admin/school-table.js +7 -8
  42. package/dist/src/web/admin/school-table.js.map +1 -1
  43. package/dist/src/web/common/api.js +32 -35
  44. package/dist/src/web/common/api.js.map +1 -1
  45. package/dist/src/web/common/converters.js +35 -36
  46. package/dist/src/web/common/converters.js.map +1 -1
  47. package/dist/src/web/common/token-login.js +11 -11
  48. package/dist/src/web/common/token-login.js.map +1 -1
  49. package/dist/src/web/entry/admin.js +5 -7
  50. package/dist/src/web/entry/admin.js.map +1 -1
  51. package/dist/src/web/entry/student-restoring.js +18 -20
  52. package/dist/src/web/entry/student-restoring.js.map +1 -1
  53. package/dist/src/web/entry/student.js +70 -76
  54. package/dist/src/web/entry/student.js.map +1 -1
  55. package/dist/src/web/entry/teacher.js +68 -68
  56. package/dist/src/web/entry/teacher.js.map +1 -1
  57. package/dist/src/web/hooks/collection.js +29 -30
  58. package/dist/src/web/hooks/collection.js.map +1 -1
  59. package/dist/src/web/hooks/document.js +35 -36
  60. package/dist/src/web/hooks/document.js.map +1 -1
  61. package/dist/src/web/hooks/subscription.js +26 -22
  62. package/dist/src/web/hooks/subscription.js.map +1 -1
  63. package/package.json +23 -19
  64. package/dist/src/functions/api/schema.js +0 -42
  65. package/dist/src/functions/api/schema.js.map +0 -1
@@ -1,136 +1,129 @@
1
- import I from "node:crypto";
2
- import { randomToken as g } from "@olinfo/quizms/utils";
3
- import { isPast as l, isFuture as T, roundToNearestMinutes as k, addSeconds as p, addMinutes as C } from "date-fns";
4
- import { getFunctions as O } from "firebase-admin/functions";
5
- import * as s from "firebase-functions/logger";
6
- import { defineString as N } from "firebase-functions/params";
7
- import { chunk as A } from "lodash-es";
8
- import { db as n, auth as m } from "../common.js";
9
- import { getParticipationStudents as w, getUser as P, getParticipation as S, getContest as _, getParticipationByToken as D } from "./queries.js";
10
- const E = N("QUIZMS_REGION"), b = async (t, a) => {
11
- s.info("Teacher login request received", { data: a });
12
- const r = await P(a.username);
13
- return r ? r.role !== "teacher" ? { success: !1, errorCode: "INVALID_ROLE", error: "Utenza non abilitata" } : r.password !== a.password ? { success: !1, errorCode: "INVALID_PASSWORD", error: "Password non corretta" } : { success: !0, data: { token: await m.createCustomToken(I.randomUUID(), {
1
+ import D from "node:crypto";
2
+ import { randomToken as E } from "@olinfo/quizms/utils";
3
+ import { TRPCError as o } from "@trpc/server";
4
+ import { isFuture as g, roundToNearestMinutes as O, addSeconds as l, addMinutes as R, isPast as h } from "date-fns";
5
+ import { getFunctions as B } from "firebase-admin/functions";
6
+ import * as d from "firebase-functions/logger";
7
+ import { defineString as F } from "firebase-functions/params";
8
+ import { chunk as P } from "lodash-es";
9
+ import r from "zod";
10
+ import { auth as T, db as c } from "../common.js";
11
+ import { getUser as S, getParticipationStudents as I, getParticipation as y, getContest as b, getParticipationByToken as v } from "./queries.js";
12
+ import { publicProcedure as w } from "./trpc.js";
13
+ const N = F("QUIZMS_REGION"), W = w.input(
14
+ r.strictObject({
15
+ username: r.string(),
16
+ password: r.string()
17
+ })
18
+ ).mutation(async (a) => {
19
+ const { input: i } = a;
20
+ d.info("Teacher login request received", { data: i });
21
+ const e = await S(i.username);
22
+ if (!e)
23
+ throw new o({ code: "BAD_REQUEST", message: "Username non esistente" });
24
+ if (e.password !== i.password)
25
+ throw new o({ code: "BAD_REQUEST", message: "Password non corretta" });
26
+ if (e.role !== "teacher")
27
+ throw new o({ code: "FORBIDDEN", message: "Utenza non abilitata" });
28
+ return T.createCustomToken(D.randomUUID(), {
14
29
  role: "teacher",
15
- schoolId: r.id
16
- }) } } : { success: !1, errorCode: "USER_NOT_FOUND", error: "Username non esistente" };
17
- };
18
- async function f(t, a) {
19
- const r = await S(t.data.participationId);
20
- if (!r)
21
- return [{ success: !1, errorCode: "PARTICIPATION_NOT_FOUND", error: "Gara non valida" }];
22
- if (r.schoolId !== t.auth?.token.schoolId)
23
- return [
24
- {
25
- success: !1,
26
- errorCode: "FORBIDDEN",
27
- error: "Gara non valida"
28
- }
29
- ];
30
- const e = await _(r.contestId);
31
- if (a) {
32
- if (!e.hasOnline)
33
- return [
34
- {
35
- success: !1,
36
- errorCode: "CONTEST_OFFLINE",
37
- error: "Gara non abilitata per prove online"
38
- }
39
- ];
40
- if (T(e.contestWindowStart))
41
- return [{ success: !1, errorCode: "CONTEST_NOT_STARTED", error: "Gara non iniziata" }];
42
- if (l(e.contestWindowEnd))
43
- return [{ success: !1, errorCode: "CONTEST_ENDED", error: "Gara già terminata" }];
30
+ schoolId: e.id
31
+ });
32
+ });
33
+ async function f(a, i, e) {
34
+ const t = await y(a);
35
+ if (!t)
36
+ throw new o({ code: "BAD_REQUEST", message: "Gara non valida" });
37
+ if (t.schoolId !== i?.schoolId)
38
+ throw new o({ code: "FORBIDDEN", message: "Gara non valida" });
39
+ const n = await b(t.contestId);
40
+ if (e) {
41
+ if (!n.hasOnline)
42
+ throw new o({ code: "FORBIDDEN", message: "Gara non abilitata per prove online" });
43
+ if (g(n.contestWindowStart))
44
+ throw new o({ code: "FORBIDDEN", message: "Gara non iniziata" });
45
+ if (h(n.contestWindowEnd))
46
+ throw new o({ code: "FORBIDDEN", message: "Gara già terminata" });
44
47
  }
45
- return [null, r, e];
48
+ return [t, n];
46
49
  }
47
- async function R() {
50
+ async function z() {
48
51
  for (; ; ) {
49
- const t = await g();
50
- if (!await D(t))
51
- return t;
52
+ const a = await E();
53
+ if (!await v(a))
54
+ return a;
52
55
  }
53
56
  }
54
- const Q = async (t, a) => {
55
- s.info("Teacher start participation received", { data: a });
56
- const [r, e, i] = await f(t, !0);
57
- if (r)
58
- return r;
59
- if (e.endingTime != null && i.hasOnline && (T(e.endingTime) || !i.allowRestart))
60
- return {
61
- success: !1,
62
- errorCode: "PARTICIPATION_ALREADY_STARTED",
63
- error: "Gara già iniziata"
64
- };
65
- if (e.token) {
66
- const d = await w(e.id, e.token);
67
- await h(d);
57
+ const L = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
58
+ const { input: i, ctx: e } = a;
59
+ d.info("Teacher start participation received", { data: i });
60
+ const [t, n] = await f(
61
+ i.participationId,
62
+ e.claims,
63
+ !0
64
+ );
65
+ if (t.endingTime != null && n.hasOnline && (g(t.endingTime) || !n.allowRestart))
66
+ throw new o({ code: "FORBIDDEN", message: "Gara già iniziata" });
67
+ if (t.token) {
68
+ const m = await I(t.id, t.token);
69
+ await k(m);
68
70
  }
69
- const c = k(p(Date.now(), 3.5 * 60)), o = C(c, i.hasOnline ? i.duration : 0), u = await R();
70
- await n.doc(`participations/${e.id}`).update({
71
+ const p = O(l(Date.now(), 3.5 * 60)), s = R(p, n.hasOnline ? n.duration : 0), u = await z();
72
+ await c.doc(`participations/${t.id}`).update({
71
73
  token: u,
72
- startingTime: c,
73
- endingTime: o
74
+ startingTime: p,
75
+ endingTime: s
74
76
  });
75
77
  try {
76
- await O().taskQueue(
77
- `locations/${E.value()}/functions/updateScores`
78
+ await B().taskQueue(
79
+ `locations/${N.value()}/functions/updateScores`
78
80
  ).enqueue(
79
- { participationId: e.id, token: u },
80
- { scheduleTime: p(o, 5) }
81
+ { participationId: t.id, token: u },
82
+ { scheduleTime: l(s, 5) }
81
83
  );
82
- } catch (d) {
83
- s.error("Failed to queue updateScores task", { error: d });
84
+ } catch (m) {
85
+ d.error("Failed to queue updateScores task", { error: m });
84
86
  }
85
- return { success: !0, data: {} };
86
- }, q = async (t, a) => {
87
- s.info("Teacher stop participation received", { data: a });
88
- const [r, e] = await f(t, !0);
89
- if (r)
90
- return r;
91
- if (e.startingTime == null)
92
- return {
93
- success: !1,
94
- errorCode: "PARTICIPATION_NOT_STARTED",
95
- error: "Gara non iniziata"
96
- };
97
- if (l(e.startingTime))
98
- return {
99
- success: !1,
100
- errorCode: "PARTICIPATION_ALREADY_STARTED",
101
- error: "Gara già iniziata"
102
- };
103
- const i = await w(e.id, e.token);
104
- await h(i);
105
- for (const c of A(i, 300)) {
106
- const o = n.batch();
107
- for (const u of c)
108
- o.delete(n.doc(`participations/${e.id}/students/${u.id}`));
109
- await o.commit();
87
+ }), Z = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
88
+ const { input: i, ctx: e } = a;
89
+ d.info("Teacher stop participation received", { data: i });
90
+ const [t] = await f(i.participationId, e.claims, !0);
91
+ if (t.startingTime == null)
92
+ throw new o({ code: "FORBIDDEN", message: "Gara non iniziata" });
93
+ if (h(t.startingTime))
94
+ throw new o({ code: "FORBIDDEN", message: "Gara già iniziata" });
95
+ const n = await I(t.id, t.token);
96
+ await k(n);
97
+ for (const p of P(n, 300)) {
98
+ const s = c.batch();
99
+ for (const u of p)
100
+ s.delete(c.doc(`participations/${t.id}/students/${u.id}`));
101
+ await s.commit();
110
102
  }
111
- return await n.doc(`participations/${e.id}`).update({
103
+ await c.doc(`participations/${t.id}`).update({
112
104
  token: null,
113
105
  startingTime: null,
114
106
  endingTime: null
115
- }), { success: !0, data: {} };
116
- };
117
- async function h(t) {
107
+ });
108
+ });
109
+ async function k(a) {
118
110
  await Promise.all(
119
- t.map(async (a) => {
120
- a.uid && await m.revokeRefreshTokens(a.uid).catch(() => {
111
+ a.map(async (i) => {
112
+ i.uid && await T.revokeRefreshTokens(i.uid).catch(() => {
121
113
  });
122
114
  })
123
115
  );
124
116
  }
125
- const M = async (t, a) => {
126
- s.info("Teacher finalize participation received", { data: a });
127
- const [r, e] = await f(t, !1);
128
- return r || (await n.doc(`participations/${e.id}`).update({ finalized: !0 }), { success: !0, data: {} });
129
- };
117
+ const H = w.input(r.strictObject({ participationId: r.string() })).mutation(async (a) => {
118
+ const { input: i, ctx: e } = a;
119
+ d.info("Teacher finalize participation received", { data: i });
120
+ const [t] = await f(i.participationId, e.claims, !1);
121
+ await c.doc(`participations/${t.id}`).update({ finalized: !0 });
122
+ });
130
123
  export {
131
- M as teacherFinalizeParticipation,
132
- b as teacherLogin,
133
- Q as teacherStartParticipation,
134
- q as teacherStopParticipation
124
+ H as teacherFinalizeParticipation,
125
+ W as teacherLogin,
126
+ L as teacherStartParticipation,
127
+ Z as teacherStopParticipation
135
128
  };
136
129
  //# sourceMappingURL=teacher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"teacher.js","sources":["../../../../src/functions/api/teacher.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { Contest, Participation, Student } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport { getFunctions } from \"firebase-admin/functions\";\nimport type { CallableRequest } from \"firebase-functions/https\";\nimport * as logger from \"firebase-functions/logger\";\nimport { defineString } from \"firebase-functions/params\";\nimport { chunk } from \"lodash-es\";\n\nimport { auth, db, type Endpoint, type EndpointError } from \"../common\";\nimport {\n getContest,\n getParticipation,\n getParticipationByToken,\n getParticipationStudents,\n getUser,\n} from \"./queries\";\nimport type {\n TeacherFinalizeParticipation,\n TeacherLogin,\n TeacherStartParticipation,\n TeacherStopParticipation,\n} from \"./schema\";\n\nconst region = defineString(\"QUIZMS_REGION\");\n\nexport const teacherLogin: Endpoint<TeacherLogin> = async (_request, data) => {\n logger.info(\"Teacher login request received\", { data });\n\n const user = await getUser(data.username);\n if (!user) {\n return { success: false, errorCode: \"USER_NOT_FOUND\", error: \"Username non esistente\" };\n }\n\n if (user.role !== \"teacher\") {\n return { success: false, errorCode: \"INVALID_ROLE\", error: \"Utenza non abilitata\" };\n }\n if (user.password !== data.password) {\n return { success: false, errorCode: \"INVALID_PASSWORD\", error: \"Password non corretta\" };\n }\n const token = await auth.createCustomToken(crypto.randomUUID(), {\n role: \"teacher\",\n schoolId: user.id,\n });\n return { success: true, data: { token } };\n};\n\nasync function checkParticipation(\n request: CallableRequest<{ participationId: string }>,\n onlineOnly: boolean,\n): Promise<[EndpointError] | [null, Participation, Contest]> {\n const participation = await getParticipation(request.data.participationId);\n if (!participation) {\n return [{ success: false, errorCode: \"PARTICIPATION_NOT_FOUND\", error: \"Gara non valida\" }];\n }\n\n if (participation.schoolId !== request.auth?.token.schoolId) {\n return [\n {\n success: false,\n errorCode: \"FORBIDDEN\",\n error: \"Gara non valida\",\n },\n ];\n }\n\n const contest = await getContest(participation.contestId);\n if (onlineOnly) {\n if (!contest.hasOnline) {\n return [\n {\n success: false,\n errorCode: \"CONTEST_OFFLINE\",\n error: \"Gara non abilitata per prove online\",\n },\n ];\n }\n if (isFuture(contest.contestWindowStart)) {\n return [{ success: false, errorCode: \"CONTEST_NOT_STARTED\", error: \"Gara non iniziata\" }];\n }\n if (isPast(contest.contestWindowEnd)) {\n return [{ success: false, errorCode: \"CONTEST_ENDED\", error: \"Gara già terminata\" }];\n }\n }\n\n return [null, participation, contest];\n}\n\nasync function generateToken() {\n for (;;) {\n const token = await randomToken();\n if (!(await getParticipationByToken(token))) {\n return token;\n }\n }\n}\n\nexport const teacherStartParticipation: Endpoint<TeacherStartParticipation> = async (\n request,\n data,\n) => {\n logger.info(\"Teacher start participation received\", { data });\n\n const [error, participation, contest] = await checkParticipation(request, true);\n if (error) {\n return error;\n }\n\n if (\n participation.endingTime != null &&\n contest.hasOnline &&\n (isFuture(participation.endingTime) || !contest.allowRestart)\n ) {\n return {\n success: false,\n errorCode: \"PARTICIPATION_ALREADY_STARTED\",\n error: \"Gara già iniziata\",\n };\n }\n\n if (participation.token) {\n const students = await getParticipationStudents(participation.id, participation.token);\n await revokeTokens(students);\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.hasOnline ? contest.duration : 0);\n\n const token = await generateToken();\n\n await db.doc(`participations/${participation.id}`).update({\n token,\n startingTime,\n endingTime,\n });\n\n try {\n const taskQueue = getFunctions().taskQueue(\n `locations/${region.value()}/functions/updateScores`,\n );\n await taskQueue.enqueue(\n { participationId: participation.id, token },\n { scheduleTime: addSeconds(endingTime, 5) },\n );\n } catch (error) {\n logger.error(\"Failed to queue updateScores task\", { error });\n }\n\n return { success: true, data: {} };\n};\n\nexport const teacherStopParticipation: Endpoint<TeacherStopParticipation> = async (\n request,\n data,\n) => {\n logger.info(\"Teacher stop participation received\", { data });\n\n const [error, participation] = await checkParticipation(request, true);\n if (error) {\n return error;\n }\n\n if (participation.startingTime == null) {\n return {\n success: false,\n errorCode: \"PARTICIPATION_NOT_STARTED\",\n error: \"Gara non iniziata\",\n };\n }\n\n if (isPast(participation.startingTime)) {\n return {\n success: false,\n errorCode: \"PARTICIPATION_ALREADY_STARTED\",\n error: \"Gara già iniziata\",\n };\n }\n\n const students = await getParticipationStudents(participation.id, participation.token!);\n await revokeTokens(students);\n\n for (const studentChunk of chunk(students, 300)) {\n const batch = db.batch();\n for (const student of studentChunk) {\n batch.delete(db.doc(`participations/${participation.id}/students/${student.id}`));\n }\n await batch.commit();\n }\n\n await db.doc(`participations/${participation.id}`).update({\n token: null,\n startingTime: null,\n endingTime: null,\n });\n\n return { success: true, data: {} };\n};\n\nasync function revokeTokens(students: Student[]) {\n await Promise.all(\n students.map(async (student) => {\n if (student.uid) {\n await auth.revokeRefreshTokens(student.uid).catch(() => {});\n }\n }),\n );\n}\n\nexport const teacherFinalizeParticipation: Endpoint<TeacherFinalizeParticipation> = async (\n request,\n data,\n) => {\n logger.info(\"Teacher finalize participation received\", { data });\n\n const [error, participation] = await checkParticipation(request, false);\n if (error) {\n return error;\n }\n\n await db.doc(`participations/${participation.id}`).update({ finalized: true });\n\n return { success: true, data: {} };\n};\n"],"names":["region","defineString","teacherLogin","_request","data","logger","user","getUser","auth","crypto","checkParticipation","request","onlineOnly","participation","getParticipation","contest","getContest","isFuture","isPast","generateToken","token","randomToken","getParticipationByToken","teacherStartParticipation","error","students","getParticipationStudents","revokeTokens","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","db","getFunctions","teacherStopParticipation","studentChunk","chunk","batch","student","teacherFinalizeParticipation"],"mappings":"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"}
1
+ {"version":3,"file":"teacher.js","sources":["../../../../src/functions/api/teacher.ts"],"sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { Contest, Participation, Student } from \"@olinfo/quizms/models\";\nimport { randomToken } from \"@olinfo/quizms/utils\";\nimport { TRPCError } from \"@trpc/server\";\nimport { addMinutes, addSeconds, isFuture, isPast, roundToNearestMinutes } from \"date-fns\";\nimport type { DecodedIdToken } from \"firebase-admin/auth\";\nimport { getFunctions } from \"firebase-admin/functions\";\nimport * as logger from \"firebase-functions/logger\";\nimport { defineString } from \"firebase-functions/params\";\nimport { chunk } from \"lodash-es\";\nimport z from \"zod\";\n\nimport { auth, db } from \"../common\";\nimport {\n getContest,\n getParticipation,\n getParticipationByToken,\n getParticipationStudents,\n getUser,\n} from \"./queries\";\nimport { publicProcedure } from \"./trpc\";\n\nconst region = defineString(\"QUIZMS_REGION\");\n\nexport const teacherLogin = publicProcedure\n .input(\n z.strictObject({\n username: z.string(),\n password: z.string(),\n }),\n )\n .mutation(async (opts) => {\n const { input: data } = opts;\n logger.info(\"Teacher login request received\", { data });\n\n const user = await getUser(data.username);\n if (!user) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Username non esistente\" });\n }\n\n if (user.password !== data.password) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Password non corretta\" });\n }\n if (user.role !== \"teacher\") {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Utenza non abilitata\" });\n }\n\n return auth.createCustomToken(crypto.randomUUID(), {\n role: \"teacher\",\n schoolId: user.id,\n });\n });\n\nasync function checkParticipation(\n participationId: string,\n claims: DecodedIdToken | undefined,\n onlineOnly: boolean,\n): Promise<[Participation, Contest]> {\n const participation = await getParticipation(participationId);\n if (!participation) {\n throw new TRPCError({ code: \"BAD_REQUEST\", message: \"Gara non valida\" });\n }\n\n if (participation.schoolId !== claims?.schoolId) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non valida\" });\n }\n\n const contest = await getContest(participation.contestId);\n if (onlineOnly) {\n if (!contest.hasOnline) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non abilitata per prove online\" });\n }\n if (isFuture(contest.contestWindowStart)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n if (isPast(contest.contestWindowEnd)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già terminata\" });\n }\n }\n\n return [participation, contest];\n}\n\nasync function generateToken() {\n for (;;) {\n const token = await randomToken();\n if (!(await getParticipationByToken(token))) {\n return token;\n }\n }\n}\n\nexport const teacherStartParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher start participation received\", { data });\n\n const [participation, contest] = await checkParticipation(\n data.participationId,\n ctx.claims,\n true,\n );\n\n if (\n participation.endingTime != null &&\n contest.hasOnline &&\n (isFuture(participation.endingTime) || !contest.allowRestart)\n ) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n if (participation.token) {\n const students = await getParticipationStudents(participation.id, participation.token);\n await revokeTokens(students);\n }\n\n const startingTime = roundToNearestMinutes(addSeconds(Date.now(), 3.5 * 60));\n const endingTime = addMinutes(startingTime, contest.hasOnline ? contest.duration : 0);\n\n const token = await generateToken();\n\n await db.doc(`participations/${participation.id}`).update({\n token,\n startingTime,\n endingTime,\n });\n\n try {\n const taskQueue = getFunctions().taskQueue(\n `locations/${region.value()}/functions/updateScores`,\n );\n await taskQueue.enqueue(\n { participationId: participation.id, token },\n { scheduleTime: addSeconds(endingTime, 5) },\n );\n } catch (error) {\n logger.error(\"Failed to queue updateScores task\", { error });\n }\n });\n\nexport const teacherStopParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher stop participation received\", { data });\n\n const [participation] = await checkParticipation(data.participationId, ctx.claims, true);\n\n if (participation.startingTime == null) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara non iniziata\" });\n }\n\n if (isPast(participation.startingTime)) {\n throw new TRPCError({ code: \"FORBIDDEN\", message: \"Gara già iniziata\" });\n }\n\n const students = await getParticipationStudents(participation.id, participation.token!);\n await revokeTokens(students);\n\n for (const studentChunk of chunk(students, 300)) {\n const batch = db.batch();\n for (const student of studentChunk) {\n batch.delete(db.doc(`participations/${participation.id}/students/${student.id}`));\n }\n await batch.commit();\n }\n\n await db.doc(`participations/${participation.id}`).update({\n token: null,\n startingTime: null,\n endingTime: null,\n });\n });\n\nasync function revokeTokens(students: Student[]) {\n await Promise.all(\n students.map(async (student) => {\n if (student.uid) {\n await auth.revokeRefreshTokens(student.uid).catch(() => {});\n }\n }),\n );\n}\n\nexport const teacherFinalizeParticipation = publicProcedure\n .input(z.strictObject({ participationId: z.string() }))\n .mutation(async (opts) => {\n const { input: data, ctx } = opts;\n logger.info(\"Teacher finalize participation received\", { data });\n\n const [participation] = await checkParticipation(data.participationId, ctx.claims, false);\n\n await db.doc(`participations/${participation.id}`).update({ finalized: true });\n });\n"],"names":["region","defineString","teacherLogin","publicProcedure","z","opts","data","logger","user","getUser","TRPCError","auth","crypto","checkParticipation","participationId","claims","onlineOnly","participation","getParticipation","contest","getContest","isFuture","isPast","generateToken","token","randomToken","getParticipationByToken","teacherStartParticipation","ctx","students","getParticipationStudents","revokeTokens","startingTime","roundToNearestMinutes","addSeconds","endingTime","addMinutes","db","getFunctions","error","teacherStopParticipation","studentChunk","chunk","batch","student","teacherFinalizeParticipation"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { initTRPC as n } from "@trpc/server";
2
+ import c from "superjson";
3
+ import { auth as a } from "../common.js";
4
+ async function p({ req: o }) {
5
+ const r = o.header("authorization");
6
+ if (r) {
7
+ const e = r.replace("Bearer ", "");
8
+ return { claims: await a.verifyIdToken(e, !0) };
9
+ }
10
+ return {};
11
+ }
12
+ const t = n.context().create({ transformer: c }), f = t.router, d = t.procedure;
13
+ export {
14
+ p as createContext,
15
+ d as publicProcedure,
16
+ f as router
17
+ };
18
+ //# sourceMappingURL=trpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trpc.js","sources":["../../../../src/functions/api/trpc.ts"],"sourcesContent":["import { initTRPC } from \"@trpc/server\";\nimport type * as trpcExpress from \"@trpc/server/adapters/express\";\nimport superjson from \"superjson\";\n\nimport { auth } from \"~/functions/common\";\n\nexport async function createContext({ req }: trpcExpress.CreateExpressContextOptions) {\n const authorization = req.header(\"authorization\");\n if (authorization) {\n const token = authorization.replace(\"Bearer \", \"\");\n const claims = await auth.verifyIdToken(token, true);\n return { claims };\n }\n return {};\n}\nexport type Context = Awaited<ReturnType<typeof createContext>>;\n\nconst t = initTRPC.context<Context>().create({ transformer: superjson });\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\n"],"names":["createContext","req","authorization","token","auth","initTRPC","superjson","router","publicProcedure"],"mappings":";;;AAMA,eAAsBA,EAAc,EAAE,KAAAC,KAAgD;AACpF,QAAMC,IAAgBD,EAAI,OAAO,eAAe;AAChD,MAAIC,GAAe;AACjB,UAAMC,IAAQD,EAAc,QAAQ,WAAW,EAAE;AAEjD,WAAO,EAAE,QADM,MAAME,EAAK,cAAcD,GAAO,EAAI,EAC1C;AAAA,EACX;AACA,SAAO,CAAA;AACT;AAGA,MAAM,IAAIE,EAAS,QAAA,EAAmB,OAAO,EAAE,aAAaC,GAAW,GAC1DC,IAAS,EAAE,QACXC,IAAkB,EAAE;"}
@@ -1,7 +1,7 @@
1
1
  import { db as d } from "./common.js";
2
2
  async function i(t) {
3
3
  const a = t.data?.after;
4
- a && await d.collection("submissions").add({
4
+ a?.exists && await d.collection("submissions").add({
5
5
  authType: t.authType,
6
6
  authId: t.authId,
7
7
  studentId: a.id,
@@ -1 +1 @@
1
- {"version":3,"file":"submissions.js","sources":["../../../src/functions/submissions.ts"],"sourcesContent":["import type { DocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Change, FirestoreAuthEvent } from \"firebase-functions/firestore\";\n\nimport { db } from \"./common\";\n\nexport async function studentUpdatedHandler(\n event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>,\n) {\n const data = event.data?.after;\n if (!data) return;\n\n await db.collection(\"submissions\").add({\n authType: event.authType,\n authId: event.authId,\n studentId: data.id,\n student: data.data(),\n submittedAt: event.time,\n });\n}\n"],"names":["studentUpdatedHandler","event","data","db"],"mappings":";AAKA,eAAsBA,EACpBC,GACA;AACA,QAAMC,IAAOD,EAAM,MAAM;AACzB,EAAKC,KAEL,MAAMC,EAAG,WAAW,aAAa,EAAE,IAAI;AAAA,IACrC,UAAUF,EAAM;AAAA,IAChB,QAAQA,EAAM;AAAA,IACd,WAAWC,EAAK;AAAA,IAChB,SAASA,EAAK,KAAA;AAAA,IACd,aAAaD,EAAM;AAAA,EAAA,CACpB;AACH;"}
1
+ {"version":3,"file":"submissions.js","sources":["../../../src/functions/submissions.ts"],"sourcesContent":["import type { DocumentSnapshot } from \"firebase-admin/firestore\";\nimport type { Change, FirestoreAuthEvent } from \"firebase-functions/firestore\";\n\nimport { db } from \"./common\";\n\nexport async function studentUpdatedHandler(\n event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>,\n) {\n const data = event.data?.after;\n if (!data?.exists) return;\n\n await db.collection(\"submissions\").add({\n authType: event.authType,\n authId: event.authId,\n studentId: data.id,\n student: data.data(),\n submittedAt: event.time,\n });\n}\n"],"names":["studentUpdatedHandler","event","data","db"],"mappings":";AAKA,eAAsBA,EACpBC,GACA;AACA,QAAMC,IAAOD,EAAM,MAAM;AACzB,EAAKC,GAAM,UAEX,MAAMC,EAAG,WAAW,aAAa,EAAE,IAAI;AAAA,IACrC,UAAUF,EAAM;AAAA,IAChB,QAAQA,EAAM;AAAA,IACd,WAAWC,EAAK;AAAA,IAChB,SAASA,EAAK,KAAA;AAAA,IACd,aAAaD,EAAM;AAAA,EAAA,CACpB;AACH;"}
@@ -1,5 +1,5 @@
1
1
  import r from "zod";
2
- const n = r.object({
2
+ const t = r.strictObject({
3
3
  id: r.string(),
4
4
  name: r.string(),
5
5
  username: r.string(),
@@ -7,6 +7,6 @@ const n = r.object({
7
7
  role: r.enum(["admin", "teacher"])
8
8
  });
9
9
  export {
10
- n as userSchema
10
+ t as userSchema
11
11
  };
12
12
  //# sourceMappingURL=user.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user.js","sources":["../../../src/models/user.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const userSchema = z.object({\n id: z.string(),\n name: z.string(),\n username: z.string(),\n password: z.string(),\n role: z.enum([\"admin\", \"teacher\"]),\n});\n\nexport type User = z.infer<typeof userSchema>;\n"],"names":["userSchema","z"],"mappings":";AAEO,MAAMA,IAAaC,EAAE,OAAO;AAAA,EACjC,IAAIA,EAAE,OAAA;AAAA,EACN,MAAMA,EAAE,OAAA;AAAA,EACR,UAAUA,EAAE,OAAA;AAAA,EACZ,UAAUA,EAAE,OAAA;AAAA,EACZ,MAAMA,EAAE,KAAK,CAAC,SAAS,SAAS,CAAC;AACnC,CAAC;"}
1
+ {"version":3,"file":"user.js","sources":["../../../src/models/user.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const userSchema = z.strictObject({\n id: z.string(),\n name: z.string(),\n username: z.string(),\n password: z.string(),\n role: z.enum([\"admin\", \"teacher\"]),\n});\n\nexport type User = z.infer<typeof userSchema>;\n"],"names":["userSchema","z"],"mappings":";AAEO,MAAMA,IAAaC,EAAE,aAAa;AAAA,EACvC,IAAIA,EAAE,OAAA;AAAA,EACN,MAAMA,EAAE,OAAA;AAAA,EACR,UAAUA,EAAE,OAAA;AAAA,EACZ,UAAUA,EAAE,OAAA;AAAA,EACZ,MAAMA,EAAE,KAAK,CAAC,SAAS,SAAS,CAAC;AACnC,CAAC;"}
@@ -1,5 +1,5 @@
1
1
  import t from "zod";
2
- const r = t.object({
2
+ const r = t.strictObject({
3
3
  id: t.string(),
4
4
  origin: t.string(),
5
5
  title: t.string(),
@@ -1 +1 @@
1
- {"version":3,"file":"website.js","sources":["../../../src/models/website.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const websiteSchema = z.object({\n id: z.string(),\n origin: z.string(),\n title: z.string(),\n contests: z.string().array(),\n});\n\nexport type Website = z.infer<typeof websiteSchema>;\n"],"names":["websiteSchema","z"],"mappings":";AAEO,MAAMA,IAAgBC,EAAE,OAAO;AAAA,EACpC,IAAIA,EAAE,OAAA;AAAA,EACN,QAAQA,EAAE,OAAA;AAAA,EACV,OAAOA,EAAE,OAAA;AAAA,EACT,UAAUA,EAAE,OAAA,EAAS,MAAA;AACvB,CAAC;"}
1
+ {"version":3,"file":"website.js","sources":["../../../src/models/website.ts"],"sourcesContent":["import z from \"zod\";\n\nexport const websiteSchema = z.strictObject({\n id: z.string(),\n origin: z.string(),\n title: z.string(),\n contests: z.string().array(),\n});\n\nexport type Website = z.infer<typeof websiteSchema>;\n"],"names":["websiteSchema","z"],"mappings":";AAEO,MAAMA,IAAgBC,EAAE,aAAa;AAAA,EAC1C,IAAIA,EAAE,OAAA;AAAA,EACN,QAAQA,EAAE,OAAA;AAAA,EACV,OAAOA,EAAE,OAAA;AAAA,EACT,UAAUA,EAAE,OAAA,EAAS,MAAA;AACvB,CAAC;"}
@@ -1,5 +1,5 @@
1
1
  import { defaultClientMainFields as e, defaultClientConditions as i } from "vite";
2
- function r() {
2
+ function t() {
3
3
  return {
4
4
  name: "quizms:firebase-preset",
5
5
  apply: "build",
@@ -7,7 +7,6 @@ function r() {
7
7
  return {
8
8
  environments: {
9
9
  firebase: {
10
- ssr: !1,
11
10
  resolve: {
12
11
  conditions: [...i],
13
12
  mainFields: [...e],
@@ -16,7 +15,7 @@ function r() {
16
15
  build: {
17
16
  rollupOptions: {
18
17
  input: {
19
- index: "virtual:quizms-entry?@olinfo/quizms-firebase/entry"
18
+ index: "virtual:quizms-entry?id=@olinfo/quizms-firebase/entry"
20
19
  }
21
20
  }
22
21
  }
@@ -27,6 +26,6 @@ function r() {
27
26
  };
28
27
  }
29
28
  export {
30
- r as default
29
+ t as default
31
30
  };
32
31
  //# sourceMappingURL=preset.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"preset.js","sources":["../../../src/vite/preset.ts"],"sourcesContent":["import { defaultClientConditions, defaultClientMainFields, type PluginOption } from \"vite\";\n\nexport default function firebasePreset(): PluginOption {\n return {\n name: \"quizms:firebase-preset\",\n apply: \"build\",\n config() {\n return {\n environments: {\n firebase: {\n ssr: false,\n resolve: {\n conditions: [...defaultClientConditions],\n mainFields: [...defaultClientMainFields],\n noExternal: true,\n },\n build: {\n rollupOptions: {\n input: {\n index: \"virtual:quizms-entry?@olinfo/quizms-firebase/entry\",\n },\n },\n },\n },\n },\n };\n },\n } satisfies PluginOption;\n}\n"],"names":["firebasePreset","defaultClientConditions","defaultClientMainFields"],"mappings":";AAEA,SAAwBA,IAA+B;AACrD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AACP,aAAO;AAAA,QACL,cAAc;AAAA,UACZ,UAAU;AAAA,YACR,KAAK;AAAA,YACL,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;AAAA,kBACL,OAAO;AAAA,gBAAA;AAAA,cACT;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"preset.js","sources":["../../../src/vite/preset.ts"],"sourcesContent":["import { defaultClientConditions, defaultClientMainFields, type PluginOption } from \"vite\";\n\nexport default function firebasePreset(): PluginOption {\n return {\n name: \"quizms:firebase-preset\",\n apply: \"build\",\n config() {\n return {\n environments: {\n firebase: {\n resolve: {\n conditions: [...defaultClientConditions],\n mainFields: [...defaultClientMainFields],\n noExternal: true,\n },\n build: {\n rollupOptions: {\n input: {\n index: \"virtual:quizms-entry?id=@olinfo/quizms-firebase/entry\",\n },\n },\n },\n },\n },\n };\n },\n } satisfies PluginOption;\n}\n"],"names":["firebasePreset","defaultClientConditions","defaultClientMainFields"],"mappings":";AAEA,SAAwBA,IAA+B;AACrD,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;AAAA,kBACL,OAAO;AAAA,gBAAA;AAAA,cACT;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
@@ -1,7 +1,7 @@
1
- import { jsxs as o, Fragment as u, jsx as e } from "react/jsx-runtime";
2
- import { useRef as f, Fragment as p, forwardRef as g, useReducer as h } from "react";
1
+ import { jsxs as o, Fragment as f, jsx as e } from "react/jsx-runtime";
2
+ import { useRef as p, Fragment as u, forwardRef as g, useReducer as h } from "react";
3
3
  import { randomId as A } from "@olinfo/quizms/utils";
4
- import { DateTime as x, CardActions as w, Button as b, Modal as y, Form as N, SelectField as z, TextField as F, TextAreaField as I, SubmitButton as v } from "@olinfo/react-components";
4
+ import { DateTime as x, CardActions as w, Button as y, Modal as N, Form as b, SelectField as z, TextField as F, TextAreaField as I, SubmitButton as v } from "@olinfo/react-components";
5
5
  import { Info as C, TriangleAlert as M, CircleX as T } from "lucide-react";
6
6
  import S from "react-markdown";
7
7
  import { announcementConverter as B } from "../common/converters.js";
@@ -14,15 +14,14 @@ import { useCollection as R } from "../hooks/collection.js";
14
14
  import "firebase/firestore";
15
15
  import { useAdmin as s } from "./context.js";
16
16
  function W() {
17
- const { contest: a } = s(), t = f(null), [r, n] = R("announcements", B, {
17
+ const { contest: a } = s(), t = p(null), [r, n] = R("announcements", B, {
18
18
  arrayConstraints: {
19
19
  contestIds: a.id
20
20
  },
21
- orderBy: ["createdAt", "desc"],
22
- subscribe: !0
21
+ orderBy: ["createdAt", "desc"]
23
22
  });
24
- return /* @__PURE__ */ o(u, { children: [
25
- /* @__PURE__ */ e("div", { className: "prose mt-4 max-w-none", children: r.map((i) => /* @__PURE__ */ o(p, { children: [
23
+ return /* @__PURE__ */ o(f, { children: [
24
+ /* @__PURE__ */ e("div", { className: "prose mt-4 max-w-none", children: r.map((i) => /* @__PURE__ */ o(u, { children: [
26
25
  /* @__PURE__ */ o("h4", { className: "flex items-center gap-3", children: [
27
26
  i.level === "info" && /* @__PURE__ */ e(C, { size: 20, className: "text-info" }),
28
27
  i.level === "warning" && /* @__PURE__ */ e(M, { size: 20, className: "text-warning" }),
@@ -33,13 +32,13 @@ function W() {
33
32
  /* @__PURE__ */ e(S, { children: i.body }),
34
33
  /* @__PURE__ */ e("hr", { className: "my-5 last:hidden" })
35
34
  ] }, i.id)) }),
36
- /* @__PURE__ */ e(w, { children: /* @__PURE__ */ e(b, { className: "btn-primary", onClick: () => t.current?.showModal(), children: "Aggiungi comunicazione" }) }),
35
+ /* @__PURE__ */ e(w, { children: /* @__PURE__ */ e(y, { className: "btn-primary", onClick: () => t.current?.showModal(), children: "Aggiungi comunicazione" }) }),
37
36
  /* @__PURE__ */ e(j, { ref: t, addAnnouncement: n })
38
37
  ] });
39
38
  }
40
39
  const j = g(function({ addAnnouncement: t }, r) {
41
40
  const { contest: n } = s(), [i, c] = h((l) => l + 1, 0);
42
- return /* @__PURE__ */ e(y, { ref: r, title: "Aggiungi comunicazione", children: /* @__PURE__ */ o(N, { onSubmit: async ({
41
+ return /* @__PURE__ */ e(N, { ref: r, title: "Aggiungi comunicazione", children: /* @__PURE__ */ o(b, { onSubmit: async ({
43
42
  level: l,
44
43
  title: m,
45
44
  body: d
@@ -1 +1 @@
1
- {"version":3,"file":"announcements.js","sources":["../../../../src/web/admin/announcements.tsx"],"sourcesContent":["import { Fragment, forwardRef, type Ref, useReducer, useRef } from \"react\";\n\nimport type { Announcement } from \"@olinfo/quizms/models\";\nimport { randomId } from \"@olinfo/quizms/utils\";\nimport {\n Button,\n CardActions,\n DateTime,\n Form,\n Modal,\n SelectField,\n SubmitButton,\n TextAreaField,\n TextField,\n} from \"@olinfo/react-components\";\nimport { CircleX, Info, TriangleAlert } from \"lucide-react\";\nimport Markdown from \"react-markdown\";\n\nimport { announcementConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function Announcements() {\n const { contest } = useAdmin();\n const ref = useRef<HTMLDialogElement>(null);\n\n const [announcements, addAnnouncement] = useCollection(\"announcements\", announcementConverter, {\n arrayConstraints: {\n contestIds: contest.id,\n },\n orderBy: [\"createdAt\", \"desc\"],\n subscribe: true,\n });\n\n return (\n <>\n <div className=\"prose mt-4 max-w-none\">\n {announcements.map((announcement) => (\n <Fragment key={announcement.id}>\n <h4 className=\"flex items-center gap-3\">\n {announcement.level === \"info\" && <Info size={20} className=\"text-info\" />}\n {announcement.level === \"warning\" && (\n <TriangleAlert size={20} className=\"text-warning\" />\n )}\n {announcement.level === \"error\" && <CircleX size={20} className=\"text-error\" />}\n {announcement.title}\n </h4>\n <small>\n <DateTime date={announcement.createdAt} />\n </small>\n <Markdown>{announcement.body}</Markdown>\n <hr className=\"my-5 last:hidden\" />\n </Fragment>\n ))}\n </div>\n <CardActions>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Aggiungi comunicazione\n </Button>\n </CardActions>\n <AnnouncementModal ref={ref} addAnnouncement={addAnnouncement} />\n </>\n );\n}\n\nconst AnnouncementModal = forwardRef(function AnnouncementModal(\n { addAnnouncement }: { addAnnouncement: (announcement: Announcement) => Promise<void> },\n ref: Ref<HTMLDialogElement>,\n) {\n const { contest } = useAdmin();\n const [id, nextId] = useReducer((prev) => prev + 1, 0);\n\n const finalize = async ({\n level,\n title,\n body,\n }: Pick<Announcement, \"level\" | \"title\" | \"body\">) => {\n try {\n await addAnnouncement({\n id: randomId(),\n createdAt: new Date(),\n contestIds: [contest.id],\n level,\n title,\n body,\n });\n nextId();\n } finally {\n if (ref && \"current\" in ref) {\n ref.current?.close();\n }\n }\n };\n\n return (\n <Modal ref={ref} title=\"Aggiungi comunicazione\">\n <Form key={id} onSubmit={finalize} className=\"!max-w-full\">\n <SelectField\n field=\"level\"\n label=\"Priorità\"\n options={{ info: \"Normale\", warning: \"Alta\" }}\n placeholder=\"Seleziona la priorità\"\n />\n <TextField field=\"title\" label=\"Titolo\" placeholder=\"Inserisci il titolo\" />\n <TextAreaField\n field=\"body\"\n label=\"Messaggio\"\n placeholder=\"Inserisci il messaggio\"\n rows={5}\n />\n <SubmitButton>Aggiungi</SubmitButton>\n </Form>\n </Modal>\n );\n});\n"],"names":["Announcements","contest","useAdmin","ref","useRef","announcements","addAnnouncement","useCollection","announcementConverter","jsxs","Fragment","jsx","announcement","Info","TriangleAlert","CircleX","DateTime","Markdown","CardActions","Button","AnnouncementModal","forwardRef","id","nextId","useReducer","prev","Modal","Form","level","title","body","randomId","SelectField","TextField","TextAreaField","SubmitButton"],"mappings":"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"}
1
+ {"version":3,"file":"announcements.js","sources":["../../../../src/web/admin/announcements.tsx"],"sourcesContent":["import { Fragment, forwardRef, type Ref, useReducer, useRef } from \"react\";\n\nimport type { Announcement } from \"@olinfo/quizms/models\";\nimport { randomId } from \"@olinfo/quizms/utils\";\nimport {\n Button,\n CardActions,\n DateTime,\n Form,\n Modal,\n SelectField,\n SubmitButton,\n TextAreaField,\n TextField,\n} from \"@olinfo/react-components\";\nimport { CircleX, Info, TriangleAlert } from \"lucide-react\";\nimport Markdown from \"react-markdown\";\n\nimport { announcementConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\n\nimport { useAdmin } from \"./context\";\n\nexport default function Announcements() {\n const { contest } = useAdmin();\n const ref = useRef<HTMLDialogElement>(null);\n\n const [announcements, addAnnouncement] = useCollection(\"announcements\", announcementConverter, {\n arrayConstraints: {\n contestIds: contest.id,\n },\n orderBy: [\"createdAt\", \"desc\"],\n });\n\n return (\n <>\n <div className=\"prose mt-4 max-w-none\">\n {announcements.map((announcement) => (\n <Fragment key={announcement.id}>\n <h4 className=\"flex items-center gap-3\">\n {announcement.level === \"info\" && <Info size={20} className=\"text-info\" />}\n {announcement.level === \"warning\" && (\n <TriangleAlert size={20} className=\"text-warning\" />\n )}\n {announcement.level === \"error\" && <CircleX size={20} className=\"text-error\" />}\n {announcement.title}\n </h4>\n <small>\n <DateTime date={announcement.createdAt} />\n </small>\n <Markdown>{announcement.body}</Markdown>\n <hr className=\"my-5 last:hidden\" />\n </Fragment>\n ))}\n </div>\n <CardActions>\n <Button className=\"btn-primary\" onClick={() => ref.current?.showModal()}>\n Aggiungi comunicazione\n </Button>\n </CardActions>\n <AnnouncementModal ref={ref} addAnnouncement={addAnnouncement} />\n </>\n );\n}\n\nconst AnnouncementModal = forwardRef(function AnnouncementModal(\n { addAnnouncement }: { addAnnouncement: (announcement: Announcement) => Promise<void> },\n ref: Ref<HTMLDialogElement>,\n) {\n const { contest } = useAdmin();\n const [id, nextId] = useReducer((prev) => prev + 1, 0);\n\n const finalize = async ({\n level,\n title,\n body,\n }: Pick<Announcement, \"level\" | \"title\" | \"body\">) => {\n try {\n await addAnnouncement({\n id: randomId(),\n createdAt: new Date(),\n contestIds: [contest.id],\n level,\n title,\n body,\n });\n nextId();\n } finally {\n if (ref && \"current\" in ref) {\n ref.current?.close();\n }\n }\n };\n\n return (\n <Modal ref={ref} title=\"Aggiungi comunicazione\">\n <Form key={id} onSubmit={finalize} className=\"!max-w-full\">\n <SelectField\n field=\"level\"\n label=\"Priorità\"\n options={{ info: \"Normale\", warning: \"Alta\" }}\n placeholder=\"Seleziona la priorità\"\n />\n <TextField field=\"title\" label=\"Titolo\" placeholder=\"Inserisci il titolo\" />\n <TextAreaField\n field=\"body\"\n label=\"Messaggio\"\n placeholder=\"Inserisci il messaggio\"\n rows={5}\n />\n <SubmitButton>Aggiungi</SubmitButton>\n </Form>\n </Modal>\n );\n});\n"],"names":["Announcements","contest","useAdmin","ref","useRef","announcements","addAnnouncement","useCollection","announcementConverter","jsxs","Fragment","jsx","announcement","Info","TriangleAlert","CircleX","DateTime","Markdown","CardActions","Button","AnnouncementModal","forwardRef","id","nextId","useReducer","prev","Modal","Form","level","title","body","randomId","SelectField","TextField","TextAreaField","SubmitButton"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as e } from "react/jsx-runtime";
2
- import { useMemo as s, Suspense as r } from "react";
2
+ import { useMemo as s, Suspense as o } from "react";
3
3
  import { AgGrid as d, Loading as c } from "@olinfo/quizms/components";
4
4
  import { participationConverter as f } from "../common/converters.js";
5
5
  import "firebase/auth";
@@ -12,18 +12,17 @@ import "firebase/firestore";
12
12
  import { useCount as p } from "../hooks/count.js";
13
13
  import { useAdmin as u } from "./context.js";
14
14
  function T() {
15
- const { contest: i } = u(), [o, n] = m(
15
+ const { contest: i } = u(), [r, n] = m(
16
16
  "participations",
17
17
  f,
18
18
  {
19
- constraints: { contestId: i.id },
20
- subscribe: !0
19
+ constraints: { contestId: i.id }
21
20
  }
22
21
  ), a = s(() => h(), []);
23
- return /* @__PURE__ */ e(r, { fallback: /* @__PURE__ */ e(c, {}), children: /* @__PURE__ */ e("div", { className: "relative grow p-2", children: /* @__PURE__ */ e("div", { className: "absolute inset-0", children: /* @__PURE__ */ e(
22
+ return /* @__PURE__ */ e(o, { fallback: /* @__PURE__ */ e(c, {}), children: /* @__PURE__ */ e("div", { className: "relative grow p-2", children: /* @__PURE__ */ e("div", { className: "absolute inset-0", children: /* @__PURE__ */ e(
24
23
  d,
25
24
  {
26
- rowData: o,
25
+ rowData: r,
27
26
  getRowId: (t) => t.data.id,
28
27
  columnDefs: a,
29
28
  onCellEditRequest: async (t) => {
@@ -55,7 +54,7 @@ function h() {
55
54
  sortable: !1,
56
55
  cellRenderer: ({ data: i }) => {
57
56
  if (i)
58
- return /* @__PURE__ */ e(r, { children: /* @__PURE__ */ e(b, { participationId: i.id }) });
57
+ return /* @__PURE__ */ e(o, { children: /* @__PURE__ */ e(w, { participationId: i.id }) });
59
58
  }
60
59
  },
61
60
  {
@@ -68,7 +67,7 @@ function h() {
68
67
  }
69
68
  ];
70
69
  }
71
- function b({ participationId: i }) {
70
+ function w({ participationId: i }) {
72
71
  return p(`participations/${i}/students`, {
73
72
  constraints: { disabled: !1 }
74
73
  });
@@ -1 +1 @@
1
- {"version":3,"file":"school-table.js","sources":["../../../../src/web/admin/school-table.tsx"],"sourcesContent":["import { Suspense, useMemo } from \"react\";\n\nimport { AgGrid, Loading } from \"@olinfo/quizms/components\";\nimport type { Participation, Student } from \"@olinfo/quizms/models\";\nimport type { CellEditRequestEvent, ColDef, ICellRendererParams } from \"ag-grid-community\";\n\nimport { participationConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\nimport { useCount } from \"~/web/hooks/count\";\n\nimport { useAdmin } from \"./context\";\n\nexport function SchoolTable() {\n const { contest } = useAdmin();\n\n const [participations, setParticipation] = useCollection(\n \"participations\",\n participationConverter,\n {\n constraints: { contestId: contest.id },\n subscribe: true,\n },\n );\n\n const colDefs = useMemo(() => columnDefinition(), []);\n\n const onCellEditRequest = async (ev: CellEditRequestEvent) => {\n const participation = ev.data as Participation;\n if (ev.colDef.field === \"finalized\") {\n await setParticipation({ ...participation, finalized: ev.newValue });\n }\n ev.api.refreshCells({ force: true });\n };\n\n return (\n <Suspense fallback={<Loading />}>\n <div className=\"relative grow p-2\">\n <div className=\"absolute inset-0\">\n <AgGrid\n rowData={participations}\n getRowId={(row) => (row.data as Participation).id}\n columnDefs={colDefs}\n onCellEditRequest={onCellEditRequest}\n />\n </div>\n </div>\n </Suspense>\n );\n}\n\nfunction columnDefinition(): ColDef[] {\n return [\n {\n field: \"schoolId\",\n headerName: \"ID\",\n width: 150,\n filter: true,\n },\n {\n field: \"name\",\n headerName: \"Nome\",\n minWidth: 200,\n flex: 1,\n filter: true,\n },\n {\n field: \"count\",\n headerName: \"Studenti\",\n width: 100,\n sortable: false,\n cellRenderer: ({ data }: ICellRendererParams<Participation>) => {\n if (!data) return;\n return (\n <Suspense>\n <Count participationId={data.id} />\n </Suspense>\n );\n },\n },\n {\n field: \"finalized\",\n headerName: \"Finalizzato\",\n width: 100,\n filter: true,\n cellDataType: \"boolean\",\n editable: true,\n },\n ];\n}\n\nfunction Count({ participationId }: { participationId: string }) {\n return useCount<Student>(`participations/${participationId}/students`, {\n constraints: { disabled: false },\n });\n}\n"],"names":["SchoolTable","contest","useAdmin","participations","setParticipation","useCollection","participationConverter","colDefs","useMemo","columnDefinition","jsx","Suspense","Loading","AgGrid","row","ev","participation","data","Count","participationId","useCount"],"mappings":";;;;;;;;;;;;;AAYO,SAASA,IAAc;AAC5B,QAAM,EAAE,SAAAC,EAAA,IAAYC,EAAA,GAEd,CAACC,GAAgBC,CAAgB,IAAIC;AAAA,IACzC;AAAA,IACAC;AAAA,IACA;AAAA,MACE,aAAa,EAAE,WAAWL,EAAQ,GAAA;AAAA,MAClC,WAAW;AAAA,IAAA;AAAA,EACb,GAGIM,IAAUC,EAAQ,MAAMC,EAAA,GAAoB,CAAA,CAAE;AAUpD,SACE,gBAAAC,EAACC,GAAA,EAAS,UAAU,gBAAAD,EAACE,GAAA,CAAA,CAAQ,GAC3B,UAAA,gBAAAF,EAAC,OAAA,EAAI,WAAU,qBACb,UAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,oBACb,UAAA,gBAAAA;AAAA,IAACG;AAAA,IAAA;AAAA,MACC,SAASV;AAAA,MACT,UAAU,CAACW,MAASA,EAAI,KAAuB;AAAA,MAC/C,YAAYP;AAAA,MACZ,mBAhBgB,OAAOQ,MAA6B;AAC5D,cAAMC,IAAgBD,EAAG;AACzB,QAAIA,EAAG,OAAO,UAAU,eACtB,MAAMX,EAAiB,EAAE,GAAGY,GAAe,WAAWD,EAAG,UAAU,GAErEA,EAAG,IAAI,aAAa,EAAE,OAAO,IAAM;AAAA,MACrC;AAAA,IAUU;AAAA,EAAA,EACF,CACF,GACF,GACF;AAEJ;AAEA,SAASN,IAA6B;AACpC,SAAO;AAAA,IACL;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ;AAAA,IAAA;AAAA,IAEV;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,MAAM;AAAA,MACN,QAAQ;AAAA,IAAA;AAAA,IAEV;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,UAAU;AAAA,MACV,cAAc,CAAC,EAAE,MAAAQ,QAA+C;AAC9D,YAAKA;AACL,mCACGN,GAAA,EACC,UAAA,gBAAAD,EAACQ,KAAM,iBAAiBD,EAAK,IAAI,EAAA,CACnC;AAAA,MAEJ;AAAA,IAAA;AAAA,IAEF;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,UAAU;AAAA,IAAA;AAAA,EACZ;AAEJ;AAEA,SAASC,EAAM,EAAE,iBAAAC,KAAgD;AAC/D,SAAOC,EAAkB,kBAAkBD,CAAe,aAAa;AAAA,IACrE,aAAa,EAAE,UAAU,GAAA;AAAA,EAAM,CAChC;AACH;"}
1
+ {"version":3,"file":"school-table.js","sources":["../../../../src/web/admin/school-table.tsx"],"sourcesContent":["import { Suspense, useMemo } from \"react\";\n\nimport { AgGrid, Loading } from \"@olinfo/quizms/components\";\nimport type { Participation, Student } from \"@olinfo/quizms/models\";\nimport type { CellEditRequestEvent, ColDef, ICellRendererParams } from \"ag-grid-community\";\n\nimport { participationConverter } from \"~/web/common/converters\";\nimport { useCollection } from \"~/web/hooks\";\nimport { useCount } from \"~/web/hooks/count\";\n\nimport { useAdmin } from \"./context\";\n\nexport function SchoolTable() {\n const { contest } = useAdmin();\n\n const [participations, setParticipation] = useCollection(\n \"participations\",\n participationConverter,\n {\n constraints: { contestId: contest.id },\n },\n );\n\n const colDefs = useMemo(() => columnDefinition(), []);\n\n const onCellEditRequest = async (ev: CellEditRequestEvent) => {\n const participation = ev.data as Participation;\n if (ev.colDef.field === \"finalized\") {\n await setParticipation({ ...participation, finalized: ev.newValue });\n }\n ev.api.refreshCells({ force: true });\n };\n\n return (\n <Suspense fallback={<Loading />}>\n <div className=\"relative grow p-2\">\n <div className=\"absolute inset-0\">\n <AgGrid\n rowData={participations}\n getRowId={(row) => (row.data as Participation).id}\n columnDefs={colDefs}\n onCellEditRequest={onCellEditRequest}\n />\n </div>\n </div>\n </Suspense>\n );\n}\n\nfunction columnDefinition(): ColDef[] {\n return [\n {\n field: \"schoolId\",\n headerName: \"ID\",\n width: 150,\n filter: true,\n },\n {\n field: \"name\",\n headerName: \"Nome\",\n minWidth: 200,\n flex: 1,\n filter: true,\n },\n {\n field: \"count\",\n headerName: \"Studenti\",\n width: 100,\n sortable: false,\n cellRenderer: ({ data }: ICellRendererParams<Participation>) => {\n if (!data) return;\n return (\n <Suspense>\n <Count participationId={data.id} />\n </Suspense>\n );\n },\n },\n {\n field: \"finalized\",\n headerName: \"Finalizzato\",\n width: 100,\n filter: true,\n cellDataType: \"boolean\",\n editable: true,\n },\n ];\n}\n\nfunction Count({ participationId }: { participationId: string }) {\n return useCount<Student>(`participations/${participationId}/students`, {\n constraints: { disabled: false },\n });\n}\n"],"names":["SchoolTable","contest","useAdmin","participations","setParticipation","useCollection","participationConverter","colDefs","useMemo","columnDefinition","jsx","Suspense","Loading","AgGrid","row","ev","participation","data","Count","participationId","useCount"],"mappings":";;;;;;;;;;;;;AAYO,SAASA,IAAc;AAC5B,QAAM,EAAE,SAAAC,EAAA,IAAYC,EAAA,GAEd,CAACC,GAAgBC,CAAgB,IAAIC;AAAA,IACzC;AAAA,IACAC;AAAA,IACA;AAAA,MACE,aAAa,EAAE,WAAWL,EAAQ,GAAA;AAAA,IAAG;AAAA,EACvC,GAGIM,IAAUC,EAAQ,MAAMC,EAAA,GAAoB,CAAA,CAAE;AAUpD,SACE,gBAAAC,EAACC,GAAA,EAAS,UAAU,gBAAAD,EAACE,GAAA,CAAA,CAAQ,GAC3B,UAAA,gBAAAF,EAAC,OAAA,EAAI,WAAU,qBACb,UAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,oBACb,UAAA,gBAAAA;AAAA,IAACG;AAAA,IAAA;AAAA,MACC,SAASV;AAAA,MACT,UAAU,CAACW,MAASA,EAAI,KAAuB;AAAA,MAC/C,YAAYP;AAAA,MACZ,mBAhBgB,OAAOQ,MAA6B;AAC5D,cAAMC,IAAgBD,EAAG;AACzB,QAAIA,EAAG,OAAO,UAAU,eACtB,MAAMX,EAAiB,EAAE,GAAGY,GAAe,WAAWD,EAAG,UAAU,GAErEA,EAAG,IAAI,aAAa,EAAE,OAAO,IAAM;AAAA,MACrC;AAAA,IAUU;AAAA,EAAA,EACF,CACF,GACF,GACF;AAEJ;AAEA,SAASN,IAA6B;AACpC,SAAO;AAAA,IACL;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ;AAAA,IAAA;AAAA,IAEV;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,MAAM;AAAA,MACN,QAAQ;AAAA,IAAA;AAAA,IAEV;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,UAAU;AAAA,MACV,cAAc,CAAC,EAAE,MAAAQ,QAA+C;AAC9D,YAAKA;AACL,mCACGN,GAAA,EACC,UAAA,gBAAAD,EAACQ,KAAM,iBAAiBD,EAAK,IAAI,EAAA,CACnC;AAAA,MAEJ;AAAA,IAAA;AAAA,IAEF;AAAA,MACE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,UAAU;AAAA,IAAA;AAAA,EACZ;AAEJ;AAEA,SAASC,EAAM,EAAE,iBAAAC,KAAgD;AAC/D,SAAOC,EAAkB,kBAAkBD,CAAe,aAAa;AAAA,IACrE,aAAa,EAAE,UAAU,GAAA;AAAA,EAAM,CAChC;AACH;"}