@goralys/core 1.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/dist/index.js ADDED
@@ -0,0 +1,879 @@
1
+ // src/hooks/useSubjects.ts
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+
4
+ // src/lib/auth/auth-event.ts
5
+ var listeners = /* @__PURE__ */ new Set();
6
+ function emitAuthEvent(event) {
7
+ listeners.forEach((l) => l(event));
8
+ }
9
+ function onAuthEvent(callback) {
10
+ listeners.add(callback);
11
+ return () => {
12
+ listeners.delete(callback);
13
+ };
14
+ }
15
+
16
+ // src/lib/fetch/navigation-event.ts
17
+ var listeners2 = /* @__PURE__ */ new Set();
18
+ function emitNavigationEvent(event) {
19
+ listeners2.forEach((l) => l(event));
20
+ }
21
+ function onNavigationEvent(listener) {
22
+ listeners2.add(listener);
23
+ return () => listeners2.delete(listener);
24
+ }
25
+
26
+ // src/lib/user/user.client.ts
27
+ var cacheUserClient;
28
+ var emptyCacheUserClient;
29
+ function configUserCacheCallbacks(cache, empty) {
30
+ cacheUserClient = cache;
31
+ emptyCacheUserClient = empty;
32
+ }
33
+ async function cacheUserDataClient() {
34
+ if (!cacheUserClient) {
35
+ throw new Error("User cache not configured. Call configUserCacheCallbacks() first.");
36
+ }
37
+ return await cacheUserClient();
38
+ }
39
+ function emptyUserCacheClient() {
40
+ if (!emptyCacheUserClient) {
41
+ throw new Error("User cache not configured. Call configUserCacheCallbacks() first.");
42
+ }
43
+ return emptyCacheUserClient();
44
+ }
45
+ async function fetchUsersClient() {
46
+ const csrfToken = await fetchCsrfClient("get-all-users");
47
+ return await goralysFetchClient("GET", buildApiUrl("users/all", { "csrf-token": csrfToken }));
48
+ }
49
+ async function fetchVirtualUsersClient() {
50
+ const csrfToken = await fetchCsrfClient("get-virtual-users");
51
+ return await goralysFetchClient("GET", buildApiUrl("users/virtual", { "csrf-token": csrfToken }));
52
+ }
53
+ async function fetchAdminsClient() {
54
+ const csrfToken = await fetchCsrfClient("get-all-admins");
55
+ return await goralysFetchClient("GET", buildApiUrl("admins/all", { "csrf-token": csrfToken }));
56
+ }
57
+ async function fetchVirtualAdminsClient() {
58
+ const csrfToken = await fetchCsrfClient("get-virtual-admins");
59
+ return await goralysFetchClient("GET", buildApiUrl("admins/virtual", { "csrf-token": csrfToken }));
60
+ }
61
+
62
+ // src/lib/auth/user-event.ts
63
+ var listeners3 = /* @__PURE__ */ new Set();
64
+ function emitUserEvent(event) {
65
+ listeners3.forEach((l) => l(event));
66
+ }
67
+ function onUserEvent(callback) {
68
+ listeners3.add(callback);
69
+ return () => {
70
+ listeners3.delete(callback);
71
+ };
72
+ }
73
+
74
+ // src/lib/fetch/goralys-action-handler.ts
75
+ var GoralysActionHandler = class {
76
+ constructor() {
77
+ this.handle = async (r) => {
78
+ const data = await r.clone().json();
79
+ if (!data || !data.action) {
80
+ return;
81
+ }
82
+ const action = data.action;
83
+ if (action === "login-success") {
84
+ await this.onLogin();
85
+ }
86
+ return;
87
+ };
88
+ this.onLogin = async () => {
89
+ await cacheUserDataClient();
90
+ emitUserEvent("login");
91
+ return;
92
+ };
93
+ }
94
+ };
95
+
96
+ // src/lib/fetch/config.ts
97
+ var config = null;
98
+ function configureGoralysClient(cfg) {
99
+ config = cfg;
100
+ }
101
+ function getGoralysClientConfig() {
102
+ if (!config) {
103
+ throw new Error("Goralys client not configured. Call configureGoralysClient() first.");
104
+ }
105
+ return config;
106
+ }
107
+
108
+ // src/lib/fetch/fetch.client.ts
109
+ var actionHandler = new GoralysActionHandler();
110
+ async function goralysFetchClient(method, input, payload, requestOptions) {
111
+ const { apiDomain } = getGoralysClientConfig();
112
+ const res = await fetch(`${apiDomain}/${input}`, {
113
+ credentials: "include",
114
+ method: method === "BREW" ? "POST" : method,
115
+ headers: method === "BREW" ? { "X-HTTP-Method-Override": method } : {},
116
+ body: payload ? payload instanceof FormData ? payload : JSON.stringify(payload) : void 0,
117
+ ...requestOptions
118
+ });
119
+ const clone = res.clone();
120
+ const contentType = clone.headers.get("Content-Type");
121
+ if (!(contentType && contentType.toLowerCase().trim().includes("application/json")))
122
+ return res;
123
+ const data = await clone.json();
124
+ if (res.status === 401) {
125
+ try {
126
+ emitAuthEvent(data?.authEvent ?? "unauthenticated");
127
+ } catch {
128
+ emitAuthEvent("unauthenticated");
129
+ }
130
+ }
131
+ if (res.status === 418) {
132
+ const teapotData = await res.clone().json();
133
+ emitNavigationEvent({
134
+ type: "teapot",
135
+ toastType: teapotData.toastType,
136
+ toastTitle: teapotData.toastTitle,
137
+ toastMessage: teapotData.toastMessage
138
+ });
139
+ }
140
+ await actionHandler.handle(res);
141
+ return res;
142
+ }
143
+ async function fetchCsrfClient(formId) {
144
+ const { apiDomain } = getGoralysClientConfig();
145
+ const res = await fetch(`${apiDomain}/csrf`, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ credentials: "include",
149
+ body: JSON.stringify({ form: formId })
150
+ });
151
+ if (!res.ok)
152
+ return null;
153
+ const json = await res.json();
154
+ return json["csrf-token"];
155
+ }
156
+ function buildQueryString(params) {
157
+ return Object.entries(params).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
158
+ }
159
+ function buildApiUrl(endpoint, params, domain = false) {
160
+ const { apiDomain } = getGoralysClientConfig();
161
+ const queryString = buildQueryString(params);
162
+ return `${domain ? apiDomain + "/" : ""}${endpoint}${queryString ? `?${queryString}` : ""}`;
163
+ }
164
+ async function handleToastRequest(r, showToast, redirect = true, duration) {
165
+ const res = r.clone();
166
+ const data = await res.json();
167
+ if (data?.toast) {
168
+ showToast({ type: data.toastType, title: data.toastTitle, message: data.toastMessage }, duration);
169
+ if (data?.redirect && redirect) {
170
+ emitNavigationEvent({ type: "redirect", url: data.redirect });
171
+ }
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ // src/lib/subjects/subjects.client.ts
178
+ async function fetchStudentSubjectsClient() {
179
+ const csrfToken = await fetchCsrfClient("get-student-subjects");
180
+ if (!csrfToken)
181
+ return null;
182
+ return await goralysFetchClient("GET", buildApiUrl("subjects/student", { "csrf-token": csrfToken }));
183
+ }
184
+ async function fetchTeacherSubjectsClient() {
185
+ const csrfToken = await fetchCsrfClient("get-teacher-subjects");
186
+ if (!csrfToken)
187
+ return null;
188
+ return await goralysFetchClient("GET", buildApiUrl("subjects/teacher", { "csrf-token": csrfToken }));
189
+ }
190
+ async function fetchAdminSubjectsClient() {
191
+ const csrfToken = await fetchCsrfClient("get-admin-subjects");
192
+ if (!csrfToken)
193
+ return null;
194
+ return await goralysFetchClient("GET", buildApiUrl("subjects/admin", { "csrf-token": csrfToken }));
195
+ }
196
+ async function fetchSubjectsForRoleClient(role) {
197
+ switch (role.role) {
198
+ case "student":
199
+ return await fetchStudentSubjectsClient();
200
+ case "teacher":
201
+ return await fetchTeacherSubjectsClient();
202
+ case "admin":
203
+ return await fetchAdminSubjectsClient();
204
+ default:
205
+ return null;
206
+ }
207
+ }
208
+
209
+ // src/lib/config.ts
210
+ var PERSISTANT_COOKIES = ["cookie-banner-dismissed"];
211
+ var PERSISTANT_LOCALS = ["goralys-cookies-expire", "flash_toast"];
212
+ var USERNAME_KEY = "username";
213
+ var ROLE_KEY = "user-role";
214
+ var FULL_NAME_KEY = "full-name";
215
+ var EMAIL_KEY = "email";
216
+ var PUB_ID_KEY = "public-id";
217
+ var SUBJECT_SYNC_RADIX = "subjects-synced-";
218
+ var SUBJECT_CACHE_RADIX = "subjects-cache-";
219
+ var USER_CACHE_RADIX = "users-cache-";
220
+ var USER_SYNC_RADIX = "users-synced-";
221
+ var ADMIN_CACHE_RADIX = "admins-cache-";
222
+ var ADMIN_SYNC_RADIX = "admins-synced-";
223
+ var SUPPORT_TICKET_CACHE = "support-tickets-cache";
224
+ var SUPPORT_TICKET_SYNC = "support-tickets-synced";
225
+ var SUBJECT_SYNCS = {
226
+ student: SUBJECT_SYNC_RADIX + "student",
227
+ teacher: SUBJECT_SYNC_RADIX + "teacher",
228
+ admin: SUBJECT_SYNC_RADIX + "admin",
229
+ none: SUBJECT_SYNC_RADIX + "unknown"
230
+ };
231
+ var SUBJECT_CACHES = {
232
+ student: SUBJECT_CACHE_RADIX + "student",
233
+ teacher: SUBJECT_CACHE_RADIX + "teacher",
234
+ admin: SUBJECT_CACHE_RADIX + "admin",
235
+ none: SUBJECT_CACHE_RADIX + "unknown"
236
+ };
237
+ var USER_SYNCS = {
238
+ "users-real": USER_SYNC_RADIX + "users",
239
+ "users-virtual": USER_SYNC_RADIX + "users-virtual",
240
+ "admins-real": ADMIN_SYNC_RADIX + "admins",
241
+ "admins-virtual": ADMIN_SYNC_RADIX + "admins-virtual"
242
+ };
243
+ var USER_CACHES = {
244
+ "users-real": USER_CACHE_RADIX + "users",
245
+ "users-virtual": USER_CACHE_RADIX + "users-virtual",
246
+ "admins-real": ADMIN_CACHE_RADIX + "admins",
247
+ "admins-virtual": ADMIN_CACHE_RADIX + "admins-virtual"
248
+ };
249
+ var helpContent = [
250
+ {
251
+ id: "getting-started",
252
+ title: "Premiers pas",
253
+ subsections: [
254
+ {
255
+ id: "gs-register",
256
+ title: "Cr\xE9ation du compte",
257
+ items: [
258
+ {
259
+ q: "Comment cr\xE9er mon compte Goralys ?",
260
+ a: `Allez sur la page d'accueil et cliquez sur "Se connecter". Cliquez ensuite sur le lien "page d'enregistrement" dans le texte sur la partie gauche. Rentrez votre identifiant fourni par l'\xE9tablissement, votre nom et mot de passe.
261
+ Note: le nom que vous entrez \xE0 ce moment appara\xEEtra sur le document officiel du Grand oral. Faites donc attention aux fautes, si toutefois, vous avez fais une erreur, contacter un administrateur qui r\xE9initialisera votre compte. Vous pourrez ensuite le recr\xE9er avec le m\xEAme identifiant.`
262
+ }
263
+ ]
264
+ },
265
+ {
266
+ id: "gs-connection",
267
+ title: "Connexion",
268
+ items: [
269
+ {
270
+ q: "Comment me connecter \xE0 Goralys ?",
271
+ a: `Allez sur la page d'accueil et cliquez sur "Se connecter". Entrez votre identifiant et mot de passe. Si vous avez oubli\xE9 votre mot de passe, contactez un administrateur de votre \xE9tablissement.`
272
+ }
273
+ ]
274
+ },
275
+ {
276
+ id: "gs-subjects",
277
+ title: "Questions",
278
+ items: [
279
+ {
280
+ q: "O\xF9 trouver mes questions de Grand oral ?",
281
+ a: 'Une fois connect\xE9, vous verrez toutes les questions disponibles via le menu "Questions" ("Mes Questions" pour les \xE9l\xE8ves) ou bien "Mes \xC9l\xE8ves" pour les professeurs.'
282
+ }
283
+ ]
284
+ }
285
+ ]
286
+ },
287
+ {
288
+ id: "students",
289
+ title: "Guide des \xE9l\xE8ves",
290
+ subsections: [
291
+ {
292
+ id: "st-consultation",
293
+ title: "Consultation des questions",
294
+ items: [
295
+ {
296
+ q: "Comment consulter mes questions ?",
297
+ a: 'Acc\xE9dez \xE0 la liste compl\xE8te de vos questions via le menu "Mes Questions". Vous pouvez ensuite les compl\xE9ter.'
298
+ },
299
+ {
300
+ q: "Comment savoir si ma question est valid\xE9e ?",
301
+ a: "L'\xE9tat de votre question est indiqu\xE9 par un code couleur :\n\u2014 Soulign\xE9e en jaune : en attente de validation par votre professeur.\n\u2014 Verte : valid\xE9e.\n\u2014 Rouge : rejet\xE9e, une modification est obligatoire."
302
+ },
303
+ {
304
+ q: "Pourquoi ma question a \xE9t\xE9 rejet\xE9e ?",
305
+ a: "Votre professeur a laiss\xE9 un commentaire expliquant le motif du rejet. Cliquez sur le menu d\xE9roulant sous votre question pour le consulter, puis modifiez votre question en cons\xE9quence."
306
+ }
307
+ ]
308
+ },
309
+ {
310
+ id: "st-send",
311
+ title: "Envoi d'une question",
312
+ items: [
313
+ {
314
+ q: "Comment joindre un brouillon \xE0 ma question ?",
315
+ a: "Au moment de l'envoi de votre question, un champ (pop-up) vous permet de joindre un fichier de brouillon. Remplissez-le avant de valider l'envoi."
316
+ }
317
+ ]
318
+ }
319
+ ]
320
+ },
321
+ {
322
+ id: "teachers",
323
+ title: "Guide des professeurs",
324
+ subsections: [
325
+ {
326
+ id: "te-gestion",
327
+ title: "Gestion des questions",
328
+ items: [
329
+ {
330
+ q: "Comment consulter les questions de mes \xE9l\xE8ves ?",
331
+ a: 'Acc\xE9dez \xE0 "Mes \xE9l\xE8ves" pour voir toutes les questions de vos \xE9l\xE8ves.'
332
+ },
333
+ {
334
+ q: "Comment trier les questions de mes \xE9l\xE8ves ?",
335
+ a: 'Vous pouvez utiliser la barre de recherche pour trier les questions de vos \xE9l\xE8ves. Vous pouvez les trier par \xE9l\xE8ve (nom de famille) ou bien par mati\xE8re. Vous pouvez utiliser des abr\xE9viations, par exemple: "nsi" pour Num\xE9rique et Sciences Informatiques, hggsp, llce, etc. \nNote: ces abr\xE9viations sont insensibles aux majuscules: nsi = NSI'
336
+ }
337
+ ]
338
+ }
339
+ ]
340
+ },
341
+ {
342
+ id: "admins",
343
+ title: "Guide des administrateurs",
344
+ subsections: [
345
+ {
346
+ id: "ad-import-format",
347
+ title: "Format d'import",
348
+ items: [
349
+ {
350
+ q: "Quel format pour importer les questions ?",
351
+ a: "Vous devez pr\xE9parer un fichier groupes.csv et un fichier CSV par classe. Consultez les exemples de format ci-dessous."
352
+ }
353
+ ],
354
+ details: [
355
+ {
356
+ title: "groupes.csv",
357
+ description: "Liste des classes avec leur(s) professeur(s) responsable(s), une ligne par groupe. Si plusieurs professeurs sont assign\xE9s \xE0 un m\xEAme groupe, s\xE9parez leur nom par un '|'. Enfin, les noms compos\xE9s doivent \xEAtre s\xE9par\xE9s par un espace.",
358
+ format: "TMATHS1, DUPONT LAMBERT Jean|PETIT Thomas\nTSVT1, DURANT Claire"
359
+ },
360
+ {
361
+ title: "TMATHS1_Math\xE9matiques.csv",
362
+ description: "Fichier de la classe : premi\xE8re ligne = titre de section, puis un \xE9l\xE8ve par ligne.",
363
+ format: "\xC9l\xE8ve\nDUBOIS Alice\nDURANT Sophie"
364
+ },
365
+ {
366
+ title: "TSVT1_Sciences et Vie de la Terre.csv",
367
+ description: "M\xEAme structure pour chaque classe d\xE9finie dans groupes.csv.",
368
+ format: "\xC9l\xE8ve\nMARTIN Olivier\nBERNARD Luc"
369
+ }
370
+ ]
371
+ },
372
+ {
373
+ id: "ad-gestion",
374
+ title: "Gestion des questions",
375
+ items: [
376
+ {
377
+ q: "Comment importer les questions ?",
378
+ a: `Allez dans "Questions" et cliquez sur "Importer les questions". T\xE9l\xE9chargez vos fichiers CSV. Un fichier "utilisateurs.txt" (identifiants/mots de passe) sera g\xE9n\xE9r\xE9 automatiquement. Ce dernier contient tous les identifiants des \xE9l\xE8ves et professeurs de l'\xE9tablissement. Vous pouvez donc l'imprimer afin de distribuer ces identifiants aux concern\xE9s.`
379
+ },
380
+ {
381
+ q: "Comment exporter toutes les questions ?",
382
+ a: 'Cliquez sur "Exporter les sujets en PDF". Un fichier ZIP contenant toutes les fiches du Grand oral sera t\xE9l\xE9charg\xE9.'
383
+ },
384
+ {
385
+ q: "Puis-je supprimer toutes les questions ?",
386
+ a: 'Oui, via "Supprimer les sujets". Une confirmation est demand\xE9e. Attention : cela supprime aussi tous les utilisateurs sauf les administrateurs.'
387
+ }
388
+ ]
389
+ },
390
+ {
391
+ id: "ad-users",
392
+ title: "Gestion des utilisateurs",
393
+ items: [
394
+ {
395
+ q: "Comment r\xE9initialiser le mot de passe d'un utilisateur ?",
396
+ a: `Allez dans "Utilisateurs" et trouvez l'utilisateur concern\xE9. Cliquez sur "R\xE9initialiser le mot de passe". Son identifiant de connexion reste le m\xEAme.`
397
+ },
398
+ {
399
+ q: "Comment supprimer un utilisateur ?",
400
+ a: `Allez dans "Utilisateurs", trouvez l'utilisateur et cliquez sur "Supprimer".
401
+ Attention : si c'est un \xE9l\xE8ve, ses questions seront \xE9galement supprim\xE9es. Si c'est un professeur alors toutes les mati\xE8res qui lui sont associ\xE9es seront \xE9galement supprim\xE9es.`
402
+ },
403
+ {
404
+ q: "Comment remplacer un professeur ?",
405
+ a: 'Allez dans "Utilisateurs", trouvez le professeur \xE0 remplacer et cliquez sur "Remplacer le professeur". Entrez le nom et pr\xE9nom du nouveau professeur. Une notification avec son identifiant appara\xEEtra et les questions des \xE9l\xE8ves du (des) groupes seront automatiquement transf\xE9r\xE9es.'
406
+ },
407
+ {
408
+ q: "Comment consulter l'identifiant d'un utilisateur ?",
409
+ a: `Dans "Utilisateurs", cliquez sur le nom de l'utilisateur, une bo\xEEte de saisie de mot de passe appara\xEEtra, entrez-y votre mot de passe puis confirmez. Vous devriez voir une notification avec l'identifiant`
410
+ },
411
+ {
412
+ q: "Comment cr\xE9er un nouvel administrateur ?",
413
+ a: `Allez dans "Acc\xE8s" pour cr\xE9er un compte administrateur. Renseignez son nom et pr\xE9nom. Une notification avec son identifiant de connexion appara\xEEtra, il pourra ensuite cr\xE9er son compte.
414
+ Attention : les administrateurs ont acc\xE8s \xE0 toutes les fonctionnalit\xE9s de gestion de l'\xE9tablissement.`
415
+ },
416
+ {
417
+ q: "Comment supprimer un administrateur ?",
418
+ a: `Dans "Acc\xE8s", trouver l'administrateur et cliquez sur "R\xE9voquer l'acc\xE8s".`
419
+ }
420
+ ]
421
+ }
422
+ ]
423
+ },
424
+ {
425
+ id: "troubleshooting",
426
+ title: "D\xE9pannage",
427
+ subsections: [
428
+ {
429
+ id: "tr-general",
430
+ title: "Probl\xE8mes courants",
431
+ items: [
432
+ {
433
+ q: "La page ne charge pas correctement",
434
+ a: "Rafra\xEEchissez la page (Ctrl+R). V\xE9rifiez votre connexion Internet. Si le probl\xE8me persiste, videz le cache ou essayez un autre navigateur."
435
+ },
436
+ {
437
+ q: "L'import de questions \xE9choue",
438
+ a: `V\xE9rifiez que vos fichiers CSV respectent bien le format d\xE9crit dans la section "Format d'import". Si vous avez acc\xE8s au serveur, veuillez consulter les logs (public_html/backend/Logs) pour plus d'informations.`
439
+ },
440
+ {
441
+ q: "Je ne vois pas les questions apr\xE8s l'import",
442
+ a: "Rafra\xEEchissez la page. Si le probl\xE8me persiste, essayez de vous d\xE9connecter puis de vous re-connecter."
443
+ }
444
+ ]
445
+ }
446
+ ]
447
+ }
448
+ ];
449
+
450
+ // src/lib/auth/check-auth.ts
451
+ var checkAuthImpl;
452
+ function configureCheckAuth(impl) {
453
+ checkAuthImpl = impl;
454
+ }
455
+ function isAuthenticated() {
456
+ if (!checkAuthImpl) {
457
+ throw new Error("Auth check not configured. Call configureAuthCheck() first.");
458
+ }
459
+ return checkAuthImpl();
460
+ }
461
+
462
+ // src/lib/storage/storage-adapter.ts
463
+ var adapter = null;
464
+ function configureStorage(customAdapter) {
465
+ adapter = customAdapter;
466
+ }
467
+ function getAdapter() {
468
+ if (!adapter) {
469
+ throw new Error("Storage not configured. Call configureStorage() first.");
470
+ }
471
+ return adapter;
472
+ }
473
+ async function storageGet(key) {
474
+ return getAdapter().getItem(key);
475
+ }
476
+ async function storageSet(key, value) {
477
+ await getAdapter().setItem(key, value);
478
+ }
479
+ async function storageRemove(key) {
480
+ await getAdapter().removeItem(key);
481
+ }
482
+
483
+ // src/hooks/useSubjects.ts
484
+ function useSubjects(role, showToast) {
485
+ const [subjects, setSubjects] = useState(null);
486
+ const showToastRef = useRef(showToast);
487
+ useEffect(() => {
488
+ showToastRef.current = showToast;
489
+ }, [showToast]);
490
+ const inFlightRef = useRef(null);
491
+ const fetchSubjects = useCallback(async () => {
492
+ const cacheKey = SUBJECT_CACHES[role];
493
+ const syncKey = SUBJECT_SYNCS[role];
494
+ console.log("[useSubjects] fetchSubjects called", { role });
495
+ if (!isAuthenticated()) {
496
+ console.log("[useSubjects] not authenticated, aborting");
497
+ return;
498
+ }
499
+ if (inFlightRef.current) {
500
+ console.log("[useSubjects] in-flight, waiting");
501
+ return inFlightRef.current;
502
+ }
503
+ let resolve;
504
+ inFlightRef.current = new Promise((r) => {
505
+ resolve = r;
506
+ });
507
+ try {
508
+ const syncValue = await storageGet(syncKey);
509
+ if (syncValue == "1") {
510
+ const raw = await storageGet(cacheKey);
511
+ if (raw === null || raw === void 0) {
512
+ await storageSet(syncKey, "0");
513
+ await storageRemove(cacheKey);
514
+ await fetchSubjects();
515
+ return;
516
+ }
517
+ const cached = JSON.parse(raw ?? "null");
518
+ setSubjects((prev) => {
519
+ if (JSON.stringify(prev) === JSON.stringify(cached))
520
+ return prev;
521
+ return cached;
522
+ });
523
+ return;
524
+ }
525
+ const res = await fetchSubjectsForRoleClient({ role });
526
+ if (res)
527
+ await handleToastRequest(res, showToastRef.current, false);
528
+ const data = await res?.json();
529
+ await storageSet(syncKey, "1");
530
+ await storageSet(cacheKey, JSON.stringify(data));
531
+ console.log("[useSubjects] set syncKey and cached to localStorage");
532
+ const result = Array.isArray(data) ? data : null;
533
+ setSubjects((prev) => {
534
+ if (JSON.stringify(prev) === JSON.stringify(result))
535
+ return prev;
536
+ return result;
537
+ });
538
+ } finally {
539
+ inFlightRef.current = null;
540
+ resolve();
541
+ }
542
+ }, [role]);
543
+ useEffect(() => {
544
+ void fetchSubjects();
545
+ }, [fetchSubjects]);
546
+ return useMemo(() => ({
547
+ subjects,
548
+ refetch: fetchSubjects,
549
+ syncKey: SUBJECT_SYNCS[role]
550
+ }), [subjects, fetchSubjects, role]);
551
+ }
552
+
553
+ // src/hooks/useSupportTickets.ts
554
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
555
+ function useSupportTickets(showToast) {
556
+ const [supportTickets, setSupportTickets] = useState2(null);
557
+ const showToastRef = useRef2(showToast);
558
+ useEffect2(() => {
559
+ showToastRef.current = showToast;
560
+ }, [showToast]);
561
+ const inFlightRef = useRef2(null);
562
+ const fetchSupportTickets = useCallback2(async () => {
563
+ const syncKey = SUPPORT_TICKET_SYNC;
564
+ const cacheKey = SUPPORT_TICKET_CACHE;
565
+ if (!isAuthenticated()) {
566
+ console.log("[useSupportTickets] not authenticated, aborting");
567
+ return;
568
+ }
569
+ if (inFlightRef.current) {
570
+ console.log("[useSupportTickets] in-flight, waiting");
571
+ return inFlightRef.current;
572
+ }
573
+ let resolve;
574
+ inFlightRef.current = new Promise((r) => {
575
+ resolve = r;
576
+ });
577
+ try {
578
+ const syncValue = await storageGet(syncKey) ?? "0";
579
+ if (syncValue == "1") {
580
+ const raw = await storageGet(cacheKey);
581
+ if (raw === null || raw === void 0) {
582
+ await storageSet(syncKey, "0");
583
+ await storageRemove(cacheKey);
584
+ await fetchSupportTickets();
585
+ return;
586
+ }
587
+ const cached = JSON.parse(raw ?? "null");
588
+ setSupportTickets((prev) => {
589
+ if (JSON.stringify(prev) === JSON.stringify(cached))
590
+ return prev;
591
+ return cached;
592
+ });
593
+ return;
594
+ }
595
+ const res = await goralysFetchClient("GET", buildApiUrl("support/tickets", { "csrf-token": await fetchCsrfClient("get-tickets") }));
596
+ if (res)
597
+ await handleToastRequest(res, showToastRef.current, false);
598
+ const data = await res?.json();
599
+ await storageSet(syncKey, "1");
600
+ await storageSet(cacheKey, JSON.stringify(data));
601
+ console.log("[useSupportTickets] set syncKey and cached to localStorage");
602
+ const result = Array.isArray(data) ? data : null;
603
+ setSupportTickets((prev) => {
604
+ if (JSON.stringify(prev) === JSON.stringify(result))
605
+ return prev;
606
+ return result;
607
+ });
608
+ } finally {
609
+ inFlightRef.current = null;
610
+ resolve();
611
+ }
612
+ }, []);
613
+ useEffect2(() => {
614
+ void fetchSupportTickets();
615
+ }, [fetchSupportTickets]);
616
+ return useMemo2(() => ({
617
+ supportTickets,
618
+ refetch: fetchSupportTickets,
619
+ syncKey: `support-tickets-synced`
620
+ }), [supportTickets, fetchSupportTickets]);
621
+ }
622
+
623
+ // src/hooks/useUsers.ts
624
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
625
+ function useUserCollection(fetchFn, type, showToast) {
626
+ const syncKey = USER_SYNCS[type];
627
+ const cacheKey = USER_CACHES[type];
628
+ const [users, setUsers] = useState3(null);
629
+ const showToastRef = useRef3(showToast);
630
+ useEffect3(() => {
631
+ showToastRef.current = showToast;
632
+ }, [showToast]);
633
+ const inFlightRef = useRef3(null);
634
+ const fetchUsers = useCallback3(async () => {
635
+ if (!isAuthenticated()) {
636
+ console.log("[useUsers] not authenticated, aborting");
637
+ return;
638
+ }
639
+ if (inFlightRef.current)
640
+ return inFlightRef.current;
641
+ let resolve;
642
+ inFlightRef.current = new Promise((r) => {
643
+ resolve = r;
644
+ });
645
+ try {
646
+ if (await storageGet(syncKey) == "1") {
647
+ const cached = JSON.parse(await storageGet(cacheKey) ?? "null");
648
+ setUsers((prev) => {
649
+ if (JSON.stringify(prev) === JSON.stringify(cached))
650
+ return prev;
651
+ return cached;
652
+ });
653
+ return;
654
+ }
655
+ const res = await fetchFn();
656
+ if (res)
657
+ await handleToastRequest(res, showToastRef.current, false);
658
+ const data = await res?.json();
659
+ if (res?.ok) {
660
+ await storageSet(syncKey, "1");
661
+ await storageSet(cacheKey, JSON.stringify(data));
662
+ }
663
+ const result = Array.isArray(data) ? data : null;
664
+ setUsers((prev) => {
665
+ if (JSON.stringify(prev) === JSON.stringify(result))
666
+ return prev;
667
+ return result;
668
+ });
669
+ } finally {
670
+ inFlightRef.current = null;
671
+ resolve();
672
+ }
673
+ }, [fetchFn, cacheKey, syncKey]);
674
+ useEffect3(() => {
675
+ void fetchUsers();
676
+ }, [fetchUsers]);
677
+ return useMemo3(() => ({ users, refetch: fetchUsers, syncKey }), [users, fetchUsers, syncKey]);
678
+ }
679
+ function useUsers(showToast) {
680
+ return useUserCollection(fetchUsersClient, "users-real", showToast);
681
+ }
682
+ function useVirtualUsers(showToast) {
683
+ return useUserCollection(fetchVirtualUsersClient, "users-virtual", showToast);
684
+ }
685
+ function useAdmins(showToast) {
686
+ return useUserCollection(fetchAdminsClient, "admins-real", showToast);
687
+ }
688
+ function useVirtualAdmins(showToast) {
689
+ return useUserCollection(fetchVirtualAdminsClient, "admins-virtual", showToast);
690
+ }
691
+
692
+ // src/lib/asset.ts
693
+ var LOGO_PATH = {
694
+ main: "m646.9,434.84c.37-3.63-.04-6.84-.6-9.99-.04-.26-.09-.52-.14-.78-2.4-13.09-9.69-23.35-19.22-32.15-3.06-2.82-6.54-5.31-10.29-7.35-6.13-3.33-12.99-5.46-19.94-5.88-.82-.68-1.66-1.36-2.51-2.03-11.22-8.86-24.4-16.29-37.96-20.47-7.2-2.22-14.48-4.31-22.09-4.37-2.58-.02-4.54.82-5.75,2.24-1.43,1.69-1.79,4.2-.85,7.08.94,2.88,7.36,26.86,13.38,46.14,2.24,7.19,4.43,13.72,6.25,18.27-4.53-5.35-10.26-13.33-14.75-24.03-3.38-8.05-5.13-15.4-6.06-21.23,0,0-2.87-.45-3.41-.57-22.83-4.92-45.77-6.06-68.6-.5-45.99,11.21-76.73,39.88-93.3,83.75-5.06,13.39-7.56,27.13-8.04,41.05-.34,9.75.33,19.6,1.82,29.47,5.82,38.7,23.83,70.43,56.16,92.81,32.08,22.21,67.37,27.69,104.76,15.63,19.71-6.36,35.86-18.07,48-34.99,13.08-18.22,17.2-38.81,16.41-60.65-.02-.69-.05-1.39-.09-2.09-.02-.41-.31-.8-.46-1.14h-80.58c-.07,1.55-.13,3.1-.17,4.64-.13,5.09-.06,10.13.21,15.43h60.53s-.27,2.67-.47,3.78c-3.02,17.01-11.46,30.89-24.58,41.9-16.04,13.46-34.65,19.14-55.5,17.17-23.68-2.23-42.57-13.18-56.3-32.46-10.8-15.15-14.85-31.72-13.84-49.1.31-5.25,1.07-10.58,2.25-15.98,4.05-18.48,11.95-35.54,20.18-52.45.36-.73.56-1.73,1.87-1.98,2.1,3.33,3.83,6.85,5.42,10.37.98,2.16,1.9,4.31,2.83,6.41-.35,1.21-1.03,1.66-1.56,2.23-13.81,14.49-21.64,30.74-22.85,48.71-.54,8.04.24,16.43,2.4,25.16,3.91,15.77,13.3,29.01,25.75,38.08,17.57,12.81,41.24,17.32,64.21,8.96,6.92-2.52,13.07-5.87,18.44-10.05h0c9.35-7.27,16.33-17.03,20.83-29.15.38-1.02.63-2.01.62-3.3h-20.18c-1.37,2.63-2.63,5.19-4.33,7.51-20.29,27.63-63.97,24.78-80.86-5.28-5.39-9.59-7.5-20.31-6.52-30.82h0c1.12-11.91,6.19-23.53,14.97-32.94,21.83-23.39,59.65-26.46,85.98-1.62,0,0,.02.02.03.03h28.52c-2.13-3.58-4.8-6.86-7.69-9.97,0,0-.01-.01-.02-.02-7.03-7.57-15.05-13.87-24.46-18.21-4-1.85-7.15-4.33-9.5-8.15-3.9-6.31-8.2-12.38-12.28-18.58-.91-1.38-1.93-2.73-2.64-4.94,2.51.71,4.99,1.42,7.45,2.12,4.13,1.18,8.22,2.35,12.27,3.51,4.7,1.34,9.36,2.67,14.02,4.01-.02.24-.05.49-.08.73-2.8.45-5.62.71-8.51.91-2.95.2-5.97.33-9.11.51,3.3,3.45,7.05,7.33,10.45,10.88,1.13,1.18,2.22,2.32,3.24,3.39.91,0,1.82,0,2.74,0,13.41.03,28.86,0,41.71-2.02,13.85-2.16,27.35-5.49,39.79-12.22,6.38-3.45,12.16-7.7,16.79-13.37.75-.92,1.76-1.76,1.65-3.14-.85-.58-1.77-.4-2.65-.4-1.81,0-3.63,0-5.44,0-7.08,0-14.15.03-21.22.04-6.86.01-13.73.23-20.59-.25-5.46-.38-10.23-2.47-14.36-6.04-.53-.46-2.14-2.27-2.14-2.27h70.5Zm-143.27-8.42c-2.89-.48-5.82-.59-8.73.04-4.91,1.07-8.52,4.28-10.07,8.46-.92,2.43-1.13,5.19-.51,8.06.74,3.45,2.32,6.56,4,9.62,1.4,2.56,2.92,5.03,4.5,7.47,1.48,2.28,2.99,4.54,4.48,6.8.6.91,2.07,3.37,2.07,3.37,0,0-16.12-2.04-35.98,6.58-4.1-5.96-15.31-21.67-17.21-23.6-5.54-5.6-12.01-6.69-19.2-3.85-4.24,1.67-7.77,4.47-11.15,7.48-8.12,7.22-14.73,15.76-21.31,24.33-.71.94-3.03,3.86-3.17,4.04.18-.49,7.79-20.98,20.23-34.32,1.64-1.77,3.38-3.42,5.2-4.87,1.44-1.16,2.93-2.19,4.47-3.08,7.99-4.59,16.45-4.63,23.82-.78,2.81,1.46,5.45,3.49,7.86,6.03,3.25,3.44,6.15,7.18,8.8,11.09,1.43,2.11,2.91,4.19,4.44,6.39,6.17-2.43,11.91-3.84,16.82-4.64-1.37-2.57-2.72-5.09-4.04-7.63-.04-.08-.08-.15-.12-.23-2.1-4.05-3.31-8.32-3.32-12.94,0-1.72.16-3.39.49-4.99,1.35-6.71,5.49-12.15,11.72-14.88,1.29-.56,2.67-1.02,4.13-1.33,14.74-3.2,27.3,1.98,38.91,11.19-1.31.92-2.28.43-3.18.27-7.98-1.35-15.95-2.78-23.93-4.1Zm74.86-24.62c2.18-9.28,10.49-15.74,21.09-15.72,10.11.01,18.6,7.03,20.85,16.46.38,1.58.58,3.24.58,4.94,0,7.36-3.59,13.67-9.21,17.38-3.41,2.25-7.57,3.54-12.12,3.53-4.57-.01-8.73-1.3-12.15-3.53-5.9-3.83-9.64-10.45-9.61-18.24,0-1.67.2-3.28.56-4.81Z",
695
+ eye: "m599.07,393.76c7.76-.04,14.2,5.96,14.22,13.26.02,7.46-6.23,13.52-14.08,13.54-7.79.02-14.18-6.07-14.2-13.46-.02-7.3,6.32-13.3,14.06-13.34Z"
696
+ };
697
+
698
+ // src/lib/cookies.ts
699
+ function setCookiesExpire(duration) {
700
+ storageSet("goralys-cookies-expire", String(Date.now() + duration * 1e3));
701
+ }
702
+
703
+ // src/lib/fools.ts
704
+ function isFoolsDay() {
705
+ const today = /* @__PURE__ */ new Date();
706
+ const formatted = today.toLocaleDateString("fr-FR", {
707
+ day: "2-digit",
708
+ month: "2-digit"
709
+ });
710
+ return formatted === "01/04";
711
+ }
712
+
713
+ // src/lib/utils.ts
714
+ function buildArray(...items) {
715
+ return items.filter((item) => Boolean(item));
716
+ }
717
+ var parsePhpDateTime = (phpDate) => {
718
+ return new Date(phpDate.date).toLocaleString("fr-FR", {
719
+ year: "numeric",
720
+ month: "2-digit",
721
+ day: "2-digit",
722
+ hour: "2-digit",
723
+ minute: "2-digit"
724
+ });
725
+ };
726
+
727
+ // src/lib/subjects/subjects-utils.ts
728
+ var SUBJECTS = [
729
+ ["apla", "arts plastiques"],
730
+ ["arts-plastiques", "arts plastiques"],
731
+ ["arts", "arts plastiques"],
732
+ ["ciav", "cin\xE9ma audiovisuel"],
733
+ ["cinema", "cin\xE9ma audiovisuel"],
734
+ ["cav", "cin\xE9ma audiovisuel"],
735
+ ["hggsp", "histoire g\xE9ographie g\xE9opolitique et sciences politiques"],
736
+ ["hlphi", "humanit\xE9 litt\xE9rature et philosophie"],
737
+ ["hlp", "humanit\xE9 litt\xE9rature et philosophie"],
738
+ ["llce", "langue litt\xE9rature et culture \xE9trang\xE8re"],
739
+ ["llcer", "langue litt\xE9rature et culture \xE9trang\xE8re"],
740
+ ["maths", "math\xE9matiques"],
741
+ ["math", "math\xE9matiques"],
742
+ ["nsinf", "num\xE9rique et sciences informatiques"],
743
+ ["nsi", "num\xE9rique et sciences informatiques"],
744
+ ["phch", "physique-chimie"],
745
+ ["pc", "physique-chimie"],
746
+ ["ses", "sciences \xE9conomiques et sociales"],
747
+ ["svt", "sciences et vie de la terre"]
748
+ ];
749
+ function getLongFromShort(short) {
750
+ const search = short.toLowerCase().trim();
751
+ const match = SUBJECTS.find(([key]) => key.startsWith(search));
752
+ return match ? match[1] : search;
753
+ }
754
+
755
+ // src/types/subjects.ts
756
+ var SubjectStatusConfig = {
757
+ not_submitted: {
758
+ label: "Non envoy\xE9e",
759
+ helper: {
760
+ student: "Cette question n'a pas encore \xE9t\xE9 envoy\xE9e.",
761
+ teacher: "L'\xE9l\xE8ve n'a pas encore envoy\xE9 cette question.",
762
+ admin: "Cette question n'a pas encore \xE9t\xE9 envoy\xE9e.",
763
+ none: "Role invalide"
764
+ }
765
+ },
766
+ submitted: {
767
+ label: "Envoy\xE9e",
768
+ helper: {
769
+ student: "Cette question est en attente de validation, vous ne pouvez plus la modifi\xE9e.",
770
+ teacher: "Cette question est en attente de validation.",
771
+ admin: "Cette question est en attente de validation.",
772
+ none: "Role invalide"
773
+ }
774
+ },
775
+ rejected: {
776
+ label: "Rejet\xE9e",
777
+ helper: {
778
+ student: "Cette question n'a pas \xE9t\xE9 valid\xE9e par le professeur, vous devez en envoyer une nouvelle.",
779
+ teacher: "Vous n'avez pas valid\xE9 cette question, l'\xE9l\xE8ve doit en envoyer une nouvelle.",
780
+ admin: "Cette question n'a pas \xE9t\xE9 valid\xE9e par le professeur, l'\xE9l\xE8ve doit en envoyer une nouvelle.",
781
+ none: "Role invalide"
782
+ }
783
+ },
784
+ approved: {
785
+ label: "Approuv\xE9e",
786
+ helper: {
787
+ student: "Cette question a \xE9t\xE9 valid\xE9e par le professeur, vous ne pouvez plus la modifier.",
788
+ teacher: "Vous avez valid\xE9e cette question, elle ne peut plus \xEAtre modifi\xE9e.",
789
+ admin: "Cette question a \xE9t\xE9 valid\xE9e par le professeur, elle ne peut plus \xEAtre modifi\xE9e.",
790
+ none: "Role invalide"
791
+ }
792
+ }
793
+ };
794
+ var searchFields = {
795
+ all: "Tout",
796
+ student: "\xC9l\xE8ves",
797
+ teacher: "Professeur",
798
+ topic: "Mati\xE8re"
799
+ };
800
+ var getStatusLabel = (status) => SubjectStatusConfig[status].label ?? "";
801
+ var getStatusHelper = (status, role) => SubjectStatusConfig[status].helper[role] ?? "";
802
+
803
+ // src/types/support.ts
804
+ var reasonsConfig = {
805
+ "password-forgot": { label: "Mot de passe oubli\xE9" },
806
+ "subject-error": { label: "Question envoy\xE9e/valid\xE9e/rejet\xE9e par erreur" },
807
+ "personal-info-error": { label: "Informations personnelles erron\xE9es (ex: Nom/Pr\xE9nom)" },
808
+ other: { label: "Autre (pr\xE9cisez)" }
809
+ };
810
+
811
+ // src/types/user.ts
812
+ var USER_ROLES = ["admin", "teacher", "student", "none"];
813
+ export {
814
+ ADMIN_CACHE_RADIX,
815
+ ADMIN_SYNC_RADIX,
816
+ EMAIL_KEY,
817
+ FULL_NAME_KEY,
818
+ GoralysActionHandler,
819
+ LOGO_PATH,
820
+ PERSISTANT_COOKIES,
821
+ PERSISTANT_LOCALS,
822
+ PUB_ID_KEY,
823
+ ROLE_KEY,
824
+ SUBJECT_CACHES,
825
+ SUBJECT_CACHE_RADIX,
826
+ SUBJECT_SYNCS,
827
+ SUBJECT_SYNC_RADIX,
828
+ SUPPORT_TICKET_CACHE,
829
+ SUPPORT_TICKET_SYNC,
830
+ SubjectStatusConfig,
831
+ USERNAME_KEY,
832
+ USER_CACHES,
833
+ USER_CACHE_RADIX,
834
+ USER_ROLES,
835
+ USER_SYNCS,
836
+ USER_SYNC_RADIX,
837
+ buildApiUrl,
838
+ buildArray,
839
+ cacheUserDataClient,
840
+ configUserCacheCallbacks,
841
+ configureCheckAuth,
842
+ configureGoralysClient,
843
+ configureStorage,
844
+ emitAuthEvent,
845
+ emitNavigationEvent,
846
+ emitUserEvent,
847
+ emptyUserCacheClient,
848
+ fetchAdminsClient,
849
+ fetchCsrfClient,
850
+ fetchSubjectsForRoleClient,
851
+ fetchUsersClient,
852
+ fetchVirtualAdminsClient,
853
+ fetchVirtualUsersClient,
854
+ getGoralysClientConfig,
855
+ getLongFromShort,
856
+ getStatusHelper,
857
+ getStatusLabel,
858
+ goralysFetchClient,
859
+ handleToastRequest,
860
+ helpContent,
861
+ isAuthenticated,
862
+ isFoolsDay,
863
+ onAuthEvent,
864
+ onNavigationEvent,
865
+ onUserEvent,
866
+ parsePhpDateTime,
867
+ reasonsConfig,
868
+ searchFields,
869
+ setCookiesExpire,
870
+ storageGet,
871
+ storageRemove,
872
+ storageSet,
873
+ useAdmins,
874
+ useSubjects,
875
+ useSupportTickets,
876
+ useUsers,
877
+ useVirtualAdmins,
878
+ useVirtualUsers
879
+ };