@goralys/ui 0.0.0-stage → 0.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.
package/dist/index.js ADDED
@@ -0,0 +1,3514 @@
1
+ // src/hooks/goralys-ui-provider.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var GoralysUIContext = createContext(null);
5
+ function GoralysUIProvider({ config, children }) {
6
+ const value = useMemo(() => config, [config]);
7
+ return /* @__PURE__ */ jsx(GoralysUIContext.Provider, { value, children });
8
+ }
9
+ function useGoralysUI() {
10
+ const context = useContext(GoralysUIContext);
11
+ if (!context) throw new Error("useGoralysUI must be used inside a GoralysUIProvider");
12
+ return context;
13
+ }
14
+
15
+ // src/hooks/use-media-query.ts
16
+ import { useEffect, useState } from "react";
17
+ function useMediaQuery(query) {
18
+ const [matches, setMatches] = useState(false);
19
+ useEffect(() => {
20
+ const run = () => {
21
+ const media = window.matchMedia(query);
22
+ setMatches(media.matches);
23
+ const listener = (e) => setMatches(e.matches);
24
+ media.addEventListener("change", listener);
25
+ return () => media.removeEventListener("change", listener);
26
+ };
27
+ return run();
28
+ }, [query]);
29
+ return matches;
30
+ }
31
+
32
+ // src/lib/inputs.ts
33
+ import { useCallback } from "react";
34
+ function useAutoResize(ref) {
35
+ return useCallback(
36
+ (el) => {
37
+ if (typeof ref === "function") {
38
+ ref(el);
39
+ } else if (ref && "current" in ref) {
40
+ Object.assign(ref, { current: el });
41
+ }
42
+ if (!el) return;
43
+ const resize = () => {
44
+ el.style.height = "auto";
45
+ el.style.height = `${el.scrollHeight}px`;
46
+ };
47
+ resize();
48
+ el.addEventListener("input", resize);
49
+ return () => el.removeEventListener("input", resize);
50
+ },
51
+ [ref]
52
+ );
53
+ }
54
+
55
+ // src/lib/modals.ts
56
+ import { useEffect as useEffect2 } from "react";
57
+ function useModalClose(modalRef, visible, onClose) {
58
+ useEffect2(() => {
59
+ if (!visible) return;
60
+ const handleClickOutside = (e) => {
61
+ if (modalRef.current && !modalRef.current.contains(e.target)) {
62
+ onClose();
63
+ }
64
+ };
65
+ const handleKeyDown = (e) => {
66
+ if (e.key === "Escape") {
67
+ onClose();
68
+ }
69
+ };
70
+ document.addEventListener("mousedown", handleClickOutside);
71
+ document.addEventListener("keydown", handleKeyDown);
72
+ return () => {
73
+ document.removeEventListener("mousedown", handleClickOutside);
74
+ document.removeEventListener("keydown", handleKeyDown);
75
+ };
76
+ }, [visible, onClose, modalRef]);
77
+ }
78
+
79
+ // src/ui/button.tsx
80
+ import clsx from "clsx";
81
+ import { jsx as jsx2 } from "react/jsx-runtime";
82
+ var colors = {
83
+ sky: "bg-sky-100 border-sky-300 before:bg-sky-300",
84
+ red: "bg-red-50 border-red-300 before:bg-red-300",
85
+ green: "bg-green-50 border-green-300 before:bg-green-300",
86
+ amber: "bg-amber-50 border-amber-300 before:bg-amber-300"
87
+ };
88
+ function Button({ text, type, className, onClick, color = "sky", disabled = false }) {
89
+ return /* @__PURE__ */ jsx2(
90
+ "button",
91
+ {
92
+ type,
93
+ className: clsx(
94
+ `
95
+ relative block w-full h-10 ${colors[color]} border mt-2 mb-2 rounded-xs shadow-lg z-0
96
+ transition-colors duration-500 overflow-hidden
97
+
98
+ before:w-0 before:h-full before:content-[''] before:absolute before:left-0 before:top-0 before:z-0
99
+ before:transition-all before:duration-500
100
+ hover:text-gray-900 hover:before:w-full
101
+ ${className}
102
+ `,
103
+ {
104
+ "bg-gray-200! border-gray-500! before:bg-transparent cursor-not-allowed": disabled
105
+ }
106
+ ),
107
+ onClick,
108
+ disabled,
109
+ children: /* @__PURE__ */ jsx2("p", { className: "relative text-[17px] z-10", children: text })
110
+ }
111
+ );
112
+ }
113
+
114
+ // src/ui/card-title.tsx
115
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
116
+ function CardTitle({ title }) {
117
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
118
+ /* @__PURE__ */ jsx3("p", { className: "text-lg", children: title }),
119
+ /* @__PURE__ */ jsx3("span", { className: "w-full bg-sky-300 h-px -mt-3 mb-1.5" })
120
+ ] });
121
+ }
122
+
123
+ // src/ui/card.tsx
124
+ import { jsx as jsx4 } from "react/jsx-runtime";
125
+ function Card({ children, className }) {
126
+ return /* @__PURE__ */ jsx4("div", { className: `flex w-full rounded-xs p-3 gap-3 ${className}`, children });
127
+ }
128
+
129
+ // src/ui/admin-panel/admin-card.tsx
130
+ import { cookiesGet, PUB_ID_KEY } from "@goralys/core";
131
+ import { ShieldExclamationIcon } from "@heroicons/react/24/outline";
132
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
133
+ function AdminCard({ admin }) {
134
+ const router = useGoralysUI().useRouter();
135
+ return /* @__PURE__ */ jsx5(Card, { className: "flex-col w-175! bg-sky-200 gap-1 p-1 mb-1 mt-1", children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-row justify-between items-center", children: [
136
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-row", children: [
137
+ /* @__PURE__ */ jsx5(ShieldExclamationIcon, { width: 27.5, className: "mr-1.5" }),
138
+ /* @__PURE__ */ jsxs2("strong", { children: [
139
+ admin.fullName,
140
+ " (",
141
+ admin.username,
142
+ ")"
143
+ ] })
144
+ ] }),
145
+ /* @__PURE__ */ jsx5("div", { className: "flex flex-row w-100 gap-1 items-center justify-end", children: cookiesGet(PUB_ID_KEY) === admin.publicId ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
146
+ /* @__PURE__ */ jsx5("p", { className: "ml-auto", children: "(vous)" }),
147
+ /* @__PURE__ */ jsx5(
148
+ Button,
149
+ {
150
+ className: "w-50!",
151
+ type: "button",
152
+ text: "Consulter votre profil",
153
+ onClick: () => router.push("/admin/user?u=" + admin.publicId)
154
+ }
155
+ )
156
+ ] }) : /* @__PURE__ */ jsx5(
157
+ Button,
158
+ {
159
+ className: "w-50!",
160
+ type: "button",
161
+ text: "G\xE9rer l'administrateur",
162
+ onClick: () => router.push("/admin/user?u=" + admin.publicId)
163
+ }
164
+ ) })
165
+ ] }) });
166
+ }
167
+
168
+ // src/ui/inputs/floating-input.tsx
169
+ import { clsx as clsx2 } from "clsx";
170
+ import { useState as useState2 } from "react";
171
+ import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
172
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
173
+ function FloatingInput({
174
+ id,
175
+ label,
176
+ helper,
177
+ autocomplete,
178
+ password = false,
179
+ email = false,
180
+ disabled = false,
181
+ required = false,
182
+ defaultValue,
183
+ value,
184
+ onInput
185
+ }) {
186
+ const [show, setShow] = useState2(!password);
187
+ const [isAutofilled, setIsAutofilled] = useState2(false);
188
+ function onEyeClicked() {
189
+ setShow(!show);
190
+ }
191
+ function handleAnimationStart(e) {
192
+ if (e.animationName === "onAutoFillStart") setIsAutofilled(true);
193
+ if (e.animationName === "onAutoFillCancel") setIsAutofilled(false);
194
+ }
195
+ return /* @__PURE__ */ jsxs3(
196
+ "div",
197
+ {
198
+ className: clsx2("relative mt-3 group min-w-50 w-full", {
199
+ "mb-5": helper !== void 0,
200
+ "mb-1": helper === void 0
201
+ }),
202
+ children: [
203
+ /* @__PURE__ */ jsx6(
204
+ "input",
205
+ {
206
+ type: email ? "email" : show ? "text" : "password",
207
+ id,
208
+ name: id,
209
+ placeholder: " ",
210
+ autoComplete: autocomplete,
211
+ disabled,
212
+ required,
213
+ defaultValue,
214
+ value,
215
+ onInput,
216
+ onAnimationStart: handleAnimationStart,
217
+ className: clsx2(
218
+ "peer block w-full leading-none py-0 px-0 cursor-text text-base text-heading bg-transparent border-0 border-b-2 border-sky-300 appearance-none focus:outline-none focus:ring-0 ",
219
+ {
220
+ "cursor-not-allowed!": disabled
221
+ }
222
+ )
223
+ }
224
+ ),
225
+ /* @__PURE__ */ jsx6(
226
+ "span",
227
+ {
228
+ className: "\n pointer-events-none\n absolute left-0 bottom-0 h-0.5 w-full\n origin-left\n bg-sky-500\n transition-transform duration-250\n group-focus-within:scale-x-100\n "
229
+ }
230
+ ),
231
+ /* @__PURE__ */ jsx6(
232
+ "label",
233
+ {
234
+ htmlFor: id,
235
+ className: clsx2(
236
+ "absolute text-base text-body cursor-text duration-300 transform -translate-y-4.5 scale-75 top-0 origin-left peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4.5 ",
237
+ {
238
+ "cursor-not-allowed!": disabled,
239
+ "scale-75! -translate-y-4.5!": isAutofilled
240
+ // force active state
241
+ }
242
+ ),
243
+ children: label
244
+ }
245
+ ),
246
+ password && /* @__PURE__ */ jsx6("button", { type: "button", onClick: onEyeClicked, className: "absolute top-px right-0 text-gray-900", children: show ? /* @__PURE__ */ jsx6(EyeSlashIcon, { className: "size-5" }) : /* @__PURE__ */ jsx6(EyeIcon, { className: "size-5" }) }),
247
+ /* @__PURE__ */ jsxs3(
248
+ "p",
249
+ {
250
+ className: clsx2("mt-0 absolute text-[13px] italic text-gray-600", {
251
+ hidden: helper === void 0
252
+ }),
253
+ children: [
254
+ "*",
255
+ helper
256
+ ]
257
+ }
258
+ )
259
+ ]
260
+ }
261
+ );
262
+ }
263
+
264
+ // src/ui/admin-panel/admins-search-bar.tsx
265
+ import { useEffect as useEffect3, useState as useState3 } from "react";
266
+ import { jsx as jsx7 } from "react/jsx-runtime";
267
+ function AdminsSearchBar({ type, admins, setCurrentAdminsAction }) {
268
+ const [searchText, setSearchText] = useState3("");
269
+ const handleSearch = (e) => {
270
+ setSearchText(e.currentTarget.value);
271
+ };
272
+ useEffect3(() => {
273
+ if (!admins) return;
274
+ const search = searchText.trim().toLowerCase();
275
+ const sorted = [...admins].filter((u) => !search || u.fullName.trim().toLowerCase().includes(search)).sort((a, b) => a.fullName.trim().toLowerCase().localeCompare(b.fullName.trim().toLowerCase(), "fr"));
276
+ setCurrentAdminsAction((prev) => {
277
+ if (JSON.stringify(prev) === JSON.stringify(sorted)) return prev;
278
+ return sorted;
279
+ });
280
+ }, [searchText, admins, setCurrentAdminsAction]);
281
+ return /* @__PURE__ */ jsx7("div", { className: "flex flex-row gap-2 items-end mb-4 w-175!", children: /* @__PURE__ */ jsx7(FloatingInput, { id: "admins-search-" + type, label: "Rechercher par nom", onInput: handleSearch }) });
282
+ }
283
+
284
+ // src/ui/admin-panel/audit-action-card.tsx
285
+ import { parsePhpDateTime } from "@goralys/core";
286
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
287
+ function AuditActionCard({ actions }) {
288
+ if (!actions) return /* @__PURE__ */ jsx8(Fragment3, {});
289
+ if (actions.length === 0) {
290
+ return /* @__PURE__ */ jsxs4(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: [
291
+ /* @__PURE__ */ jsx8(CardTitle, { title: "Actions" }),
292
+ /* @__PURE__ */ jsx8("p", { children: "Aucune action enregistr\xE9e pour cet administrateur" })
293
+ ] });
294
+ }
295
+ return /* @__PURE__ */ jsxs4(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: [
296
+ /* @__PURE__ */ jsx8(CardTitle, { title: "Actions" }),
297
+ actions.map((a) => /* @__PURE__ */ jsxs4("div", { className: "w-full h-5 flex flex-row items-center", children: [
298
+ /* @__PURE__ */ jsx8("p", { className: "mr-2 text-xs font-mono", children: parsePhpDateTime(a.at) }),
299
+ /* @__PURE__ */ jsx8("p", { children: a.description })
300
+ ] }, a.initiator + parsePhpDateTime(a.at)))
301
+ ] });
302
+ }
303
+
304
+ // src/ui/admin-panel/auth-token-card.tsx
305
+ import { buildApiUrl, fetchCsrfClient, goralysFetchClient, handleToastRequest, parsePhpDateTime as parsePhpDateTime2 } from "@goralys/core";
306
+
307
+ // src/ui/toast/toast-provider.tsx
308
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect4, useMemo as useMemo2, useState as useState4 } from "react";
309
+
310
+ // src/ui/toast/toast-element.tsx
311
+ import { clsx as clsx3 } from "clsx";
312
+ import { CheckCircleIcon, ExclamationCircleIcon, InformationCircleIcon, XCircleIcon } from "@heroicons/react/24/outline";
313
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
314
+ var icons = {
315
+ info: /* @__PURE__ */ jsx9(InformationCircleIcon, { className: "size-14 p-0 -ml-1.5 -mr-1.5 text-blue-600" }),
316
+ success: /* @__PURE__ */ jsx9(CheckCircleIcon, { className: "size-14 p-0 -ml-1.5 -mr-1.5 text-green-600" }),
317
+ warning: /* @__PURE__ */ jsx9(ExclamationCircleIcon, { className: "size-14 p-0 -ml-1.5 -mr-1.5 text-amber-600" }),
318
+ error: /* @__PURE__ */ jsx9(XCircleIcon, { className: "size-14 p-0 -ml-1.5 -mr-1.5 text-red-600" })
319
+ };
320
+ function ToastElement({ type, title, message, visible }) {
321
+ return /* @__PURE__ */ jsx9(
322
+ "div",
323
+ {
324
+ className: clsx3(
325
+ "fixed flex gap-2 p-3 h-22 sm:w-115 w-95 bg-sky-300 rounded shadow overflow-hidden left-1/2 -translate-x-1/2 top-1 ",
326
+ "after:absolute after:left-0 after:top-0 after:h-full after:w-1.25 after:content-['']",
327
+ "transition-all duration-500 z-10",
328
+ {
329
+ "after:bg-blue-600": type === "info",
330
+ "after:bg-green-600": type === "success",
331
+ "after:bg-amber-600": type === "warning",
332
+ "after:bg-red-600": type === "error"
333
+ },
334
+ {
335
+ "translate-y-0 opacity-100": visible,
336
+ "-translate-y-5 opacity-0": !visible
337
+ }
338
+ ),
339
+ role: "alert",
340
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex gap-3", children: [
341
+ /* @__PURE__ */ jsx9("div", { className: "flex self-center", children: icons[type] }),
342
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col justify-center", children: [
343
+ /* @__PURE__ */ jsx9("strong", { className: "text-md", children: title }),
344
+ /* @__PURE__ */ jsx9("span", { className: "text-sm", children: message })
345
+ ] })
346
+ ] })
347
+ }
348
+ );
349
+ }
350
+
351
+ // src/ui/toast/toast-provider.tsx
352
+ import { FLASH_TOAST_KEY, storageSet } from "@goralys/core";
353
+ import { createPortal } from "react-dom";
354
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
355
+ var ToastContext = createContext2(null);
356
+ function ToastProvider({ children }) {
357
+ const [toast, setToast] = useState4(null);
358
+ const [visible, setVisible] = useState4(false);
359
+ const cacheToast = useCallback2((toastInput, duration = 5500) => {
360
+ if (!toastInput.expires) toastInput.expires = Date.now() + duration;
361
+ storageSet(FLASH_TOAST_KEY, JSON.stringify(toastInput));
362
+ }, []);
363
+ const showToast = useCallback2(
364
+ (toastInput, animate = true, duration = 5500) => {
365
+ cacheToast(toastInput, duration);
366
+ if (!toastInput.expires) toastInput.expires = Date.now() + duration;
367
+ if (toastInput.expires === toast?.expires) return;
368
+ setToast(toastInput);
369
+ setVisible(!animate);
370
+ if (animate) requestAnimationFrame(() => setVisible(true));
371
+ },
372
+ [cacheToast, toast]
373
+ );
374
+ useEffect4(() => {
375
+ if (!toast || !toast.expires) return;
376
+ const left = toast.expires - Date.now();
377
+ const hide = setTimeout(() => setVisible(false), left - 500);
378
+ const clear = setTimeout(() => setToast(null), left);
379
+ return () => {
380
+ clearTimeout(hide);
381
+ clearTimeout(clear);
382
+ };
383
+ }, [toast]);
384
+ const value = useMemo2(() => ({ showToast, cacheToast }), [showToast, cacheToast]);
385
+ return /* @__PURE__ */ jsxs6(ToastContext.Provider, { value, children: [
386
+ children,
387
+ toast && typeof document !== "undefined" && createPortal(/* @__PURE__ */ jsx10(ToastElement, { ...toast, visible }), document.getElementById("toast-root"))
388
+ ] });
389
+ }
390
+ function useToast() {
391
+ const context = useContext2(ToastContext);
392
+ if (!context) {
393
+ throw new Error("useToast must be used in a valid context.");
394
+ }
395
+ return context;
396
+ }
397
+
398
+ // src/ui/modals/confirm/confirm-provider.tsx
399
+ import { createContext as createContext3, useCallback as useCallback3, useContext as useContext3, useMemo as useMemo3, useState as useState5 } from "react";
400
+
401
+ // src/ui/modals/confirm/confirm-element.tsx
402
+ import { clsx as clsx4 } from "clsx";
403
+ import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
404
+ import { useRef } from "react";
405
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
406
+ function ConfirmElement({ title, message, visible, onConfirmAction, onCancelAction }) {
407
+ const modalRef = useRef(null);
408
+ useModalClose(modalRef, visible, onCancelAction);
409
+ return /* @__PURE__ */ jsxs7(
410
+ "div",
411
+ {
412
+ ref: modalRef,
413
+ className: clsx4(
414
+ "fixed flex flex-col gap-2 p-3 sm:w-115 w-96 bg-sky-200 rounded shadow overflow-hidden left-1/2 -translate-x-1/2 top-1 ",
415
+ "after:absolute after:left-0 after:top-0 after:h-full after:w-1.25 after:content-[''] after:bg-blue-500",
416
+ "transition-all duration-500 z-50 ",
417
+ {
418
+ "translate-y-0 opacity-100": visible,
419
+ "-translate-y-5 opacity-0": !visible
420
+ }
421
+ ),
422
+ role: "dialog",
423
+ "aria-modal": "true",
424
+ children: [
425
+ /* @__PURE__ */ jsxs7("div", { className: "flex gap-3", children: [
426
+ /* @__PURE__ */ jsx11("div", { className: "w-11 h-11 flex self-center items-center justify-center", children: /* @__PURE__ */ jsx11(QuestionMarkCircleIcon, { className: "size-15 text-blue-500" }) }),
427
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col justify-center flex-1", children: [
428
+ /* @__PURE__ */ jsx11("strong", { className: "text-md", children: title }),
429
+ /* @__PURE__ */ jsx11("span", { className: "text-sm", children: message })
430
+ ] })
431
+ ] }),
432
+ /* @__PURE__ */ jsxs7("div", { className: "flex justify-between gap-2 mt-2", children: [
433
+ /* @__PURE__ */ jsx11(
434
+ Button,
435
+ {
436
+ className: "bg-gray-400! before:bg-gray-500! text-white! border-none! shadow-none! mt-0! mb-0! w-25!",
437
+ text: "Annuler",
438
+ type: "button",
439
+ onClick: onCancelAction
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx11(
443
+ Button,
444
+ {
445
+ className: "bg-blue-500! before:bg-blue-600! text-white! border-none! shadow-none! mt-0! mb-0! w-25!",
446
+ text: "Confirmer",
447
+ type: "button",
448
+ onClick: onConfirmAction
449
+ }
450
+ )
451
+ ] })
452
+ ]
453
+ }
454
+ );
455
+ }
456
+
457
+ // src/ui/modals/confirm/confirm-provider.tsx
458
+ import { createPortal as createPortal2 } from "react-dom";
459
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
460
+ var ConfirmContext = createContext3(null);
461
+ function ConfirmProvider({ children }) {
462
+ const [confirmState, setConfirmState] = useState5(null);
463
+ const [visible, setVisible] = useState5(false);
464
+ const showConfirm = useCallback3((options) => {
465
+ return new Promise((resolve) => {
466
+ setConfirmState({ options, resolve });
467
+ setVisible(false);
468
+ requestAnimationFrame(() => setVisible(true));
469
+ });
470
+ }, []);
471
+ function handleConfirm() {
472
+ setVisible(false);
473
+ setTimeout(() => {
474
+ confirmState?.resolve(true);
475
+ setConfirmState(null);
476
+ }, 500);
477
+ }
478
+ function handleCancel() {
479
+ setVisible(false);
480
+ setTimeout(() => {
481
+ confirmState?.resolve(false);
482
+ setConfirmState(null);
483
+ }, 500);
484
+ }
485
+ const value = useMemo3(() => ({ showConfirm }), [showConfirm]);
486
+ return /* @__PURE__ */ jsxs8(ConfirmContext.Provider, { value, children: [
487
+ children,
488
+ confirmState && typeof document !== "undefined" && createPortal2(
489
+ /* @__PURE__ */ jsx12("div", { className: "fixed inset-0 z-40 bg-black/20 backdrop-blur-sm", children: /* @__PURE__ */ jsx12(
490
+ ConfirmElement,
491
+ {
492
+ ...confirmState.options,
493
+ visible,
494
+ onConfirmAction: handleConfirm,
495
+ onCancelAction: handleCancel
496
+ }
497
+ ) }),
498
+ document.getElementById("confirm-root")
499
+ )
500
+ ] });
501
+ }
502
+ function useConfirm() {
503
+ const context = useContext3(ConfirmContext);
504
+ if (!context) {
505
+ throw new Error("useConfirm must be used within a ConfirmProvider");
506
+ }
507
+ return context;
508
+ }
509
+
510
+ // src/ui/admin-panel/auth-token-card.tsx
511
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
512
+ function AuthTokenCard({ publicId, token, onUpdate }) {
513
+ const { showToast } = useToast();
514
+ const { showConfirm } = useConfirm();
515
+ const revoke = async () => {
516
+ if (!await showConfirm({ title: "R\xE9vocation du jeton", message: "Voulez-vous vraiment r\xE9voquer ce jeton d'authentification ?" }))
517
+ return;
518
+ const res = await goralysFetchClient(
519
+ "DELETE",
520
+ buildApiUrl("user/token/any", {
521
+ "csrf-token": await fetchCsrfClient("revoke-auth-token"),
522
+ name: token.name,
523
+ target: publicId
524
+ }),
525
+ void 0,
526
+ { suppressRedirect: true }
527
+ );
528
+ await handleToastRequest(res, showToast, false);
529
+ await onUpdate();
530
+ };
531
+ return /* @__PURE__ */ jsxs9(Card, { className: "grid grid-cols-[1fr_auto] grid-rows-[auto_1fr] gap-x-4", children: [
532
+ /* @__PURE__ */ jsx13("p", { className: "py-2", children: token.username }),
533
+ /* @__PURE__ */ jsx13("p", { className: "font-bold py-2 text-right", children: token.name }),
534
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-col mt-2", children: [
535
+ /* @__PURE__ */ jsxs9("p", { className: "italic text-sm", children: [
536
+ "Date de cr\xE9ation: ",
537
+ parsePhpDateTime2(token.created)
538
+ ] }),
539
+ /* @__PURE__ */ jsxs9("p", { className: "italic text-sm", children: [
540
+ "Date d'expiration: ",
541
+ parsePhpDateTime2(token.expires)
542
+ ] })
543
+ ] }),
544
+ /* @__PURE__ */ jsx13("div", { className: "flex items-center", children: /* @__PURE__ */ jsx13(Button, { text: "R\xE9voquer", type: "button", color: "red", className: "w-[105%]! min-w-25 mt-0! mb-0!", onClick: revoke }) })
545
+ ] });
546
+ }
547
+
548
+ // src/ui/admin-panel/create-admin-element.tsx
549
+ import { useState as useState8 } from "react";
550
+
551
+ // src/ui/modals/password/password-modal-provider.tsx
552
+ import { createContext as createContext4, useCallback as useCallback4, useContext as useContext4, useMemo as useMemo4, useState as useState7 } from "react";
553
+
554
+ // src/ui/modals/password/password-modal-element.tsx
555
+ import { LockClosedIcon } from "@heroicons/react/16/solid";
556
+ import { clsx as clsx5 } from "clsx";
557
+ import { useRef as useRef2, useState as useState6 } from "react";
558
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
559
+ function PasswordModalElement({ visible, onConfirmAction, onCancelAction, onCloseModalAction, message }) {
560
+ const [password, setPassword] = useState6("");
561
+ const modalRef = useRef2(null);
562
+ useModalClose(modalRef, visible, onCloseModalAction);
563
+ const onConfirm = () => {
564
+ onConfirmAction(password);
565
+ setPassword("");
566
+ };
567
+ const onCancel = () => {
568
+ setPassword("");
569
+ onCancelAction();
570
+ };
571
+ return /* @__PURE__ */ jsxs10(
572
+ "div",
573
+ {
574
+ ref: modalRef,
575
+ className: clsx5(
576
+ "fixed flex flex-col gap-2 p-3 w-125 bg-sky-200 rounded shadow overflow-hidden left-1/2 -translate-x-1/2 top-1",
577
+ "after:absolute after:left-0 after:top-0 after:h-full after:w-1.25 after:content-[''] after:bg-blue-500",
578
+ "transition-all duration-500 z-50",
579
+ {
580
+ "translate-y-0 opacity-100": visible,
581
+ "-translate-y-5 opacity-0": !visible
582
+ }
583
+ ),
584
+ role: "dialog",
585
+ "aria-modal": "true",
586
+ children: [
587
+ /* @__PURE__ */ jsxs10("div", { className: "flex gap-3", children: [
588
+ /* @__PURE__ */ jsx14("div", { className: "w-11 h-11 flex self-center items-center justify-center", children: /* @__PURE__ */ jsx14(LockClosedIcon, { className: "size-15 text-blue-500" }) }),
589
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-col justify-center flex-1", children: [
590
+ /* @__PURE__ */ jsx14("strong", { className: "text-md", children: "Confirmation requise" }),
591
+ /* @__PURE__ */ jsx14("span", { className: "text-sm", children: message })
592
+ ] })
593
+ ] }),
594
+ /* @__PURE__ */ jsx14(
595
+ FloatingInput,
596
+ {
597
+ id: "confirm-password",
598
+ label: "Mot de passe",
599
+ password: true,
600
+ autocomplete: "current-password",
601
+ onInput: (e) => setPassword(e.currentTarget.value)
602
+ }
603
+ ),
604
+ /* @__PURE__ */ jsxs10("div", { className: "flex justify-between gap-2 mt-2", children: [
605
+ /* @__PURE__ */ jsx14(
606
+ Button,
607
+ {
608
+ className: "bg-gray-400! before:bg-gray-500! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
609
+ text: "Annuler",
610
+ type: "button",
611
+ onClick: onCancel
612
+ }
613
+ ),
614
+ /* @__PURE__ */ jsx14(
615
+ Button,
616
+ {
617
+ className: "bg-blue-500! before:bg-blue-600! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
618
+ text: "Confirmer",
619
+ type: "button",
620
+ onClick: onConfirm
621
+ }
622
+ )
623
+ ] })
624
+ ]
625
+ }
626
+ );
627
+ }
628
+
629
+ // src/ui/modals/password/password-modal-provider.tsx
630
+ import { createPortal as createPortal3 } from "react-dom";
631
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
632
+ var PasswordModalContext = createContext4(null);
633
+ function PasswordModalProvider({ children }) {
634
+ const [state, setState] = useState7(null);
635
+ const [visible, setVisible] = useState7(false);
636
+ const [message, setMessage] = useState7("");
637
+ const DEFAULT_PASSWORD_CONFIRM = "cette action";
638
+ const showPasswordModal = useCallback4((confirm = DEFAULT_PASSWORD_CONFIRM) => {
639
+ setMessage("Veuillez entrer votre mot de passe pour confirmer " + confirm.replace(".", " ").trim() + ".");
640
+ return new Promise((resolve) => {
641
+ setState({ resolve });
642
+ setVisible(false);
643
+ requestAnimationFrame(() => setVisible(true));
644
+ });
645
+ }, []);
646
+ function handleConfirm(password) {
647
+ setVisible(false);
648
+ setTimeout(() => {
649
+ state?.resolve(password);
650
+ setState(null);
651
+ }, 500);
652
+ }
653
+ function handleCancel() {
654
+ setVisible(false);
655
+ setTimeout(() => {
656
+ state?.resolve(null);
657
+ setState(null);
658
+ }, 500);
659
+ }
660
+ function handleClose() {
661
+ setVisible(false);
662
+ setTimeout(() => {
663
+ state?.resolve(null);
664
+ setState(null);
665
+ }, 500);
666
+ }
667
+ const value = useMemo4(() => ({ showPasswordModal }), [showPasswordModal]);
668
+ return /* @__PURE__ */ jsxs11(PasswordModalContext.Provider, { value, children: [
669
+ children,
670
+ state && typeof document !== "undefined" && createPortal3(
671
+ /* @__PURE__ */ jsx15("div", { className: "fixed inset-0 z-40 bg-black/20 backdrop-blur-sm", children: /* @__PURE__ */ jsx15(
672
+ PasswordModalElement,
673
+ {
674
+ message,
675
+ visible,
676
+ onConfirmAction: handleConfirm,
677
+ onCancelAction: handleCancel,
678
+ onCloseModalAction: handleClose
679
+ }
680
+ ) }),
681
+ document.getElementById("password-modal-root")
682
+ )
683
+ ] });
684
+ }
685
+ function usePasswordModal() {
686
+ const context = useContext4(PasswordModalContext);
687
+ if (!context) {
688
+ throw new Error("usePasswordModal must be used within a EmailModalProvider");
689
+ }
690
+ return context;
691
+ }
692
+
693
+ // src/ui/admin-panel/create-admin-element.tsx
694
+ import { cookiesSet, fetchCsrfClient as fetchCsrfClient2, goralysFetchClient as goralysFetchClient2, handleToastRequest as handleToastRequest2 } from "@goralys/core";
695
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
696
+ function CreateAdminElement({ onUpdateAction, syncKey, virtualSyncKey }) {
697
+ const [firstName, setFirstName] = useState8("");
698
+ const [lastName, setLastName] = useState8("");
699
+ const password = usePasswordModal();
700
+ const toast = useToast();
701
+ const createAdmin = async () => {
702
+ const pwd = await password.showPasswordModal("la cr\xE9ation du nouvel administrateur");
703
+ if (!pwd) return;
704
+ if (pwd.trim() === "") {
705
+ toast.showToast({
706
+ type: "warning",
707
+ title: "Mot de passe",
708
+ message: "Veuillez saisir un mot de passe."
709
+ });
710
+ return;
711
+ }
712
+ const csrfToken = await fetchCsrfClient2("create-admin");
713
+ const payload = {
714
+ "first-name": firstName,
715
+ "last-name": lastName,
716
+ "admin-password": pwd,
717
+ "csrf-token": csrfToken
718
+ };
719
+ const res = await goralysFetchClient2("POST", "admin/create", payload);
720
+ await handleToastRequest2(res, toast.showToast, false);
721
+ const data = await res?.json();
722
+ if (data.toastType === "info" && res.ok) {
723
+ cookiesSet(syncKey, "0");
724
+ cookiesSet(virtualSyncKey, "0");
725
+ onUpdateAction();
726
+ }
727
+ };
728
+ return /* @__PURE__ */ jsxs12("details", { className: "group/details", children: [
729
+ /* @__PURE__ */ jsxs12("summary", { className: "flex flex-row cursor-pointer", children: [
730
+ /* @__PURE__ */ jsx16(
731
+ "svg",
732
+ {
733
+ className: "w-5 h-5 text-gray-900 transition group-open/details:rotate-90",
734
+ xmlns: "http://www.w3.org/2000/svg",
735
+ width: "16",
736
+ height: "16",
737
+ fill: "currentColor",
738
+ viewBox: "0 0 16 16",
739
+ children: /* @__PURE__ */ jsx16(
740
+ "path",
741
+ {
742
+ fillRule: "evenodd",
743
+ d: "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
744
+ }
745
+ )
746
+ }
747
+ ),
748
+ /* @__PURE__ */ jsx16("span", { children: "Cr\xE9er un administrateur" })
749
+ ] }),
750
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-2 mt-2", children: [
751
+ /* @__PURE__ */ jsx16("p", { className: "-mb-2", children: "Entrer le nom du nouvel administrateur" }),
752
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-row min-w-full gap-x-2.5", children: [
753
+ /* @__PURE__ */ jsx16(FloatingInput, { id: "create-admin-firstname", label: "Pr\xE9nom", onInput: (e) => setFirstName(e.currentTarget.value) }),
754
+ /* @__PURE__ */ jsx16(FloatingInput, { id: "create-admin-lastname", label: "Nom", onInput: (e) => setLastName(e.currentTarget.value) })
755
+ ] })
756
+ ] }),
757
+ /* @__PURE__ */ jsx16(Button, { type: "button", text: "Cr\xE9er", onClick: createAdmin })
758
+ ] });
759
+ }
760
+
761
+ // src/ui/admin-panel/replace-teacher-element.tsx
762
+ import { useState as useState9 } from "react";
763
+
764
+ // src/ui/basic/dropdown.tsx
765
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
766
+ function Dropdown({ label, children, openDefault = false }) {
767
+ return /* @__PURE__ */ jsxs13("details", { className: "group/details", open: openDefault, children: [
768
+ /* @__PURE__ */ jsxs13("summary", { className: "flex flex-row cursor-pointer", children: [
769
+ /* @__PURE__ */ jsx17(
770
+ "svg",
771
+ {
772
+ className: "w-5 h-5 text-gray-900 transition group-open/details:rotate-90",
773
+ xmlns: "http://www.w3.org/2000/svg",
774
+ width: "16",
775
+ height: "16",
776
+ fill: "currentColor",
777
+ viewBox: "0 0 16 16",
778
+ children: /* @__PURE__ */ jsx17(
779
+ "path",
780
+ {
781
+ fillRule: "evenodd",
782
+ d: "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
783
+ }
784
+ )
785
+ }
786
+ ),
787
+ typeof label === "object" ? label : /* @__PURE__ */ jsx17("span", { children: label })
788
+ ] }),
789
+ children
790
+ ] });
791
+ }
792
+
793
+ // src/ui/admin-panel/replace-teacher-element.tsx
794
+ import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
795
+ function ReplaceTeacherElement({ onReplaceAction, dropDown = true }) {
796
+ const [firstName, setFirstName] = useState9("");
797
+ const [lastName, setLastName] = useState9("");
798
+ return dropDown ? /* @__PURE__ */ jsxs14(Dropdown, { label: "Remplacer le professeur", children: [
799
+ /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-2 mt-2", children: [
800
+ /* @__PURE__ */ jsx18("p", { className: "-mb-2", children: "Entrer le nom du nouveau professeur" }),
801
+ /* @__PURE__ */ jsxs14("div", { className: "flex flex-row min-w-full gap-x-2.5", children: [
802
+ /* @__PURE__ */ jsx18(FloatingInput, { id: "replace-teacher-firstname", label: "Pr\xE9nom", onInput: (e) => setFirstName(e.currentTarget.value) }),
803
+ /* @__PURE__ */ jsx18(FloatingInput, { id: "replace-teacher-lastname", label: "Nom", onInput: (e) => setLastName(e.currentTarget.value) })
804
+ ] })
805
+ ] }),
806
+ /* @__PURE__ */ jsx18(Button, { type: "button", text: "Remplacer", onClick: () => onReplaceAction(firstName, lastName) })
807
+ ] }) : /* @__PURE__ */ jsxs14(Fragment4, { children: [
808
+ /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-2 mt-2", children: [
809
+ /* @__PURE__ */ jsx18("p", { className: "-mb-2", children: "Entrer le nom du nouveau professeur" }),
810
+ /* @__PURE__ */ jsx18(FloatingInput, { id: "replace-teacher-firstname", label: "Pr\xE9nom", onInput: (e) => setFirstName(e.currentTarget.value) }),
811
+ /* @__PURE__ */ jsx18(FloatingInput, { id: "replace-teacher-lastname", label: "Nom", onInput: (e) => setLastName(e.currentTarget.value) })
812
+ ] }),
813
+ /* @__PURE__ */ jsx18(Button, { type: "button", text: "Remplacer", onClick: () => onReplaceAction(firstName, lastName) })
814
+ ] });
815
+ }
816
+
817
+ // src/ui/admin-panel/user-card.tsx
818
+ import { AcademicCapIcon, BookOpenIcon } from "@heroicons/react/24/outline";
819
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
820
+ function UserCard({ user }) {
821
+ const router = useGoralysUI().useRouter();
822
+ return /* @__PURE__ */ jsx19(Card, { className: "flex-col w-200! bg-sky-200 gap-1 p-1 mb-1 mt-1", children: /* @__PURE__ */ jsxs15("div", { className: "flex flex-row justify-between items-center", children: [
823
+ /* @__PURE__ */ jsxs15("div", {
824
+ className: "flex flex-row",
825
+ // No admins here.
826
+ children: [
827
+ user.role == "teacher" ? /* @__PURE__ */ jsx19(BookOpenIcon, { width: 27.5, className: "mr-1.5" }) : /* @__PURE__ */ jsx19(AcademicCapIcon, { width: 27.5, className: "mr-1.5" }),
828
+ /* @__PURE__ */ jsxs15("strong", { children: [
829
+ user.fullName.length > 25 ? user.fullName.substring(0, 24) + "..." : user.fullName,
830
+ " (",
831
+ user.username,
832
+ ")"
833
+ ] })
834
+ ]
835
+ }),
836
+ /* @__PURE__ */ jsx19("div", { className: "flex flex-row w-100 gap-1 place-content-end", children: /* @__PURE__ */ jsx19(
837
+ Button,
838
+ {
839
+ type: "button",
840
+ className: "w-50!",
841
+ text: "G\xE9rer le compte",
842
+ onClick: () => router.push("/admin/user?u=" + user.publicId)
843
+ }
844
+ ) })
845
+ ] }) });
846
+ }
847
+
848
+ // src/ui/admin-panel/users-search-bar.tsx
849
+ import { useEffect as useEffect5, useState as useState10 } from "react";
850
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
851
+ var roleFields = {
852
+ all: "Tous",
853
+ student: "\xC9l\xE8ves",
854
+ teacher: "Professeurs"
855
+ };
856
+ function UsersSearchBar({ type, users, setCurrentUsers }) {
857
+ const [currentRole, setCurrentRole] = useState10("all");
858
+ const [searchText, setSearchText] = useState10("");
859
+ const handleSearch = (e) => {
860
+ setSearchText(e.currentTarget.value);
861
+ };
862
+ const sortUsers = (list) => {
863
+ return [...list].sort((a, b) => {
864
+ if (a.role !== b.role) return a.role === "teacher" ? -1 : 1;
865
+ return a.fullName.trim().toLowerCase().localeCompare(b.fullName.trim().toLowerCase(), "fr");
866
+ });
867
+ };
868
+ useEffect5(() => {
869
+ if (!users) return;
870
+ const search = searchText.trim().toLowerCase();
871
+ const sorted = sortUsers(
872
+ users.filter((u) => {
873
+ const matchesRole = currentRole === "all" || u.role === currentRole;
874
+ const matchesName = !search || u.fullName.trim().toLowerCase().includes(search);
875
+ return matchesRole && matchesName;
876
+ })
877
+ );
878
+ setCurrentUsers((prev) => {
879
+ if (JSON.stringify(prev) === JSON.stringify(sorted)) return prev;
880
+ return sorted;
881
+ });
882
+ }, [searchText, currentRole, users, setCurrentUsers]);
883
+ return /* @__PURE__ */ jsxs16("div", { className: "flex flex-row gap-2 items-end mb-4 w-175!", children: [
884
+ /* @__PURE__ */ jsx20(FloatingInput, { id: "users-search-" + type, label: "Rechercher par nom", onInput: handleSearch }),
885
+ /* @__PURE__ */ jsxs16("div", { className: "relative pb-0 mb-1", children: [
886
+ /* @__PURE__ */ jsx20(
887
+ "select",
888
+ {
889
+ className: "border-0 border-b-2 border-sky-300 appearance-none\n cursor-pointer outline-none focus:ring-0 text-base leading-5\n text-heading pb-0 pr-5 subjects-search-select",
890
+ value: currentRole,
891
+ onChange: (e) => setCurrentRole(e.target.value),
892
+ children: Object.entries(roleFields).map(([key, label]) => /* @__PURE__ */ jsx20("option", { value: key, children: label }, key))
893
+ }
894
+ ),
895
+ /* @__PURE__ */ jsx20(
896
+ "span",
897
+ {
898
+ className: "absolute bottom-0 left-0 h-0.5 bg-sky-500\n transition-all duration-300 ease-in-out\n peer-focus:w-full subjects-search-underline"
899
+ }
900
+ )
901
+ ] })
902
+ ] });
903
+ }
904
+
905
+ // src/ui/admin-panel/subjects/status-legend.tsx
906
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
907
+ var STATUS_LABELS = {
908
+ not_submitted: { label: "Brouillon", color: "#2563EB" },
909
+ submitted: { label: "Envoy\xE9e", color: "#D97706" },
910
+ approved: { label: "Valid\xE9e", color: "#059669" },
911
+ rejected: { label: "Rejet\xE9e", color: "#DC2626" }
912
+ };
913
+ function StatusLegend({ total, submitted, approved, rejected, notSubmitted }) {
914
+ const counts = {
915
+ not_submitted: notSubmitted,
916
+ submitted,
917
+ approved,
918
+ rejected
919
+ };
920
+ return /* @__PURE__ */ jsx21("ul", { className: "flex flex-col gap-1 self-center mr-10", children: Object.keys(STATUS_LABELS).map((key) => /* @__PURE__ */ jsxs17("li", { className: "flex items-center gap-2 text-sm", children: [
921
+ /* @__PURE__ */ jsx21(
922
+ "span",
923
+ {
924
+ className: "inline-block w-2.5 h-2.5 rounded-full shrink-0",
925
+ style: { backgroundColor: STATUS_LABELS[key].color }
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx21("span", { children: STATUS_LABELS[key].label }),
929
+ /* @__PURE__ */ jsxs17("span", { className: "text-gray-500 ml-auto", children: [
930
+ counts[key],
931
+ " / ",
932
+ total
933
+ ] })
934
+ ] }, key)) });
935
+ }
936
+
937
+ // src/ui/admin-panel/subjects/subject-students-donut.tsx
938
+ import { useEffect as useEffect6, useState as useState11 } from "react";
939
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
940
+ var STATUS_CONFIG = {
941
+ empty: { percent: 0, color: "#D4D4D8" },
942
+ // zinc-300
943
+ not_submitted: { percent: 33, color: "#2563EB" },
944
+ // blue-600
945
+ submitted: { percent: 66, color: "#D97706" },
946
+ // amber-600
947
+ approved: { percent: 100, color: "#059669" },
948
+ // green-600
949
+ rejected: { percent: 100, color: "#DC2626" }
950
+ // red-600
951
+ };
952
+ var TRACK_COLOR = "#D4D4D8";
953
+ function SubjectStudentsDonut({ status, size = 48 }) {
954
+ const { percent, color } = STATUS_CONFIG[status];
955
+ const strokeWidth = size * 0.15;
956
+ const radius = (size - strokeWidth) / 2;
957
+ const circumference = 2 * Math.PI * radius;
958
+ const [offset, setOffset] = useState11(circumference);
959
+ useEffect6(() => {
960
+ const targetOffset = circumference - percent / 100 * circumference;
961
+ const raf = requestAnimationFrame(() => setOffset(targetOffset));
962
+ return () => cancelAnimationFrame(raf);
963
+ }, [percent, circumference]);
964
+ return /* @__PURE__ */ jsxs18("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, children: [
965
+ /* @__PURE__ */ jsx22("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: TRACK_COLOR, strokeWidth }),
966
+ /* @__PURE__ */ jsx22(
967
+ "circle",
968
+ {
969
+ cx: size / 2,
970
+ cy: size / 2,
971
+ r: radius,
972
+ fill: "none",
973
+ stroke: color,
974
+ strokeWidth,
975
+ strokeDasharray: circumference,
976
+ strokeDashoffset: offset,
977
+ strokeLinecap: "round",
978
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
979
+ style: { transition: "stroke-dashoffset 0.6s ease-out" }
980
+ }
981
+ )
982
+ ] });
983
+ }
984
+
985
+ // src/ui/admin-panel/subjects/subject-teachers-donut.tsx
986
+ import { useEffect as useEffect7, useState as useState12 } from "react";
987
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
988
+ var STATUS_CONFIG2 = {
989
+ empty: { color: "#D4D4D8" },
990
+ // zinc-300
991
+ not_submitted: { color: "#2563EB" },
992
+ // blue-600
993
+ submitted: { color: "#D97706" },
994
+ // amber-600
995
+ approved: { color: "#059669" },
996
+ // green-600
997
+ rejected: { color: "#DC2626" }
998
+ // red-600
999
+ };
1000
+ var TRACK_COLOR2 = "#D4D4D8";
1001
+ function SubjectTeachersDonut({ size = 192, total, notSubmitted, submitted, rejected, approved }) {
1002
+ const strokeWidth = size * 0.15;
1003
+ const radius = (size - strokeWidth) / 2;
1004
+ const circumference = 2 * Math.PI * radius;
1005
+ const approvedPercent = approved / total;
1006
+ const rejectedPercent = rejected / total;
1007
+ const submittedPercent = submitted / total;
1008
+ const notSubmittedPercent = notSubmitted / total;
1009
+ const [notSubmittedOffset, setNotSubmittedOffset] = useState12(circumference);
1010
+ const [submittedOffset, setSubmittedOffset] = useState12(circumference);
1011
+ const [rejectedOffset, setRejectedOffset] = useState12(circumference);
1012
+ const [approvedOffset, setApprovedOffset] = useState12(circumference);
1013
+ useEffect7(() => {
1014
+ const raf = requestAnimationFrame(() => {
1015
+ let sum = approvedPercent;
1016
+ setApprovedOffset(circumference - sum * circumference);
1017
+ sum += rejectedPercent;
1018
+ setRejectedOffset(circumference - sum * circumference);
1019
+ sum += submittedPercent;
1020
+ setSubmittedOffset(circumference - sum * circumference);
1021
+ setNotSubmittedOffset(0);
1022
+ });
1023
+ return () => cancelAnimationFrame(raf);
1024
+ }, [circumference, approvedPercent, rejectedPercent, submittedPercent]);
1025
+ const circles = [
1026
+ { status: "not_submitted", color: STATUS_CONFIG2["not_submitted"].color, percent: notSubmittedPercent, offset: notSubmittedOffset },
1027
+ { status: "submitted", color: STATUS_CONFIG2["submitted"].color, percent: submittedPercent, offset: submittedOffset },
1028
+ { status: "rejected", color: STATUS_CONFIG2["rejected"].color, percent: rejectedPercent, offset: rejectedOffset },
1029
+ { status: "approved", color: STATUS_CONFIG2["approved"].color, percent: approvedPercent, offset: approvedOffset }
1030
+ ];
1031
+ let delay = circles.filter((c) => c.percent > 0).length;
1032
+ const ANIM_DURATION = 0.6;
1033
+ return /* @__PURE__ */ jsxs19("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "mr-2", children: [
1034
+ /* @__PURE__ */ jsx23("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: TRACK_COLOR2, strokeWidth }),
1035
+ circles.map(
1036
+ (c) => c.percent > 0 && /* @__PURE__ */ jsx23(
1037
+ "circle",
1038
+ {
1039
+ cx: size / 2,
1040
+ cy: size / 2,
1041
+ r: radius,
1042
+ fill: "none",
1043
+ stroke: c.color,
1044
+ strokeWidth,
1045
+ strokeDasharray: circumference,
1046
+ strokeDashoffset: c.offset,
1047
+ strokeLinecap: "butt",
1048
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
1049
+ style: { transition: `stroke-dashoffset ${ANIM_DURATION}s ease-out ${ANIM_DURATION * delay--}s` }
1050
+ },
1051
+ c.status
1052
+ )
1053
+ )
1054
+ ] });
1055
+ }
1056
+
1057
+ // src/ui/admin-panel/subjects/user-subjects-student-card.tsx
1058
+ import { getShortFromLong, getStatusLabel } from "@goralys/core";
1059
+ import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
1060
+ function UserSubjectsStudentCard({ subjects }) {
1061
+ return /* @__PURE__ */ jsx24(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: subjects?.map((s, i) => {
1062
+ const realStatus = s.subject.trim() === "" && s.status === "not_submitted" ? "empty" : s.status;
1063
+ return /* @__PURE__ */ jsxs20(Card, { className: "flex flex-row w-full justify-between", children: [
1064
+ /* @__PURE__ */ jsxs20("div", { className: "flex flex-col", children: [
1065
+ /* @__PURE__ */ jsx24("p", { className: "text-lg font-bold", children: getShortFromLong(s.topic) }),
1066
+ /* @__PURE__ */ jsxs20("p", { className: "italic", children: [
1067
+ "Statut: ",
1068
+ getStatusLabel(s.status)
1069
+ ] })
1070
+ ] }),
1071
+ /* @__PURE__ */ jsx24(SubjectStudentsDonut, { status: realStatus })
1072
+ ] }, s.teacherToken + "#" + i);
1073
+ }) });
1074
+ }
1075
+
1076
+ // src/ui/admin-panel/subjects/user-subjects-teacher-card.tsx
1077
+ import { Fragment as Fragment5, jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
1078
+ function UserSubjectsTeacherCard({ subjects }) {
1079
+ if (!subjects) return /* @__PURE__ */ jsx25(Fragment5, {});
1080
+ const total = subjects.length;
1081
+ const notSubmitted = subjects.filter((s) => s.status === "not_submitted").length;
1082
+ const submitted = subjects.filter((s) => s.status === "submitted").length;
1083
+ const rejected = subjects.filter((s) => s.status === "rejected").length;
1084
+ const approved = subjects.filter((s) => s.status === "approved").length;
1085
+ return /* @__PURE__ */ jsxs21(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: [
1086
+ /* @__PURE__ */ jsx25(CardTitle, { title: "Sujets" }),
1087
+ /* @__PURE__ */ jsxs21(Card, { className: "flex flex-row w-full", children: [
1088
+ /* @__PURE__ */ jsx25(
1089
+ SubjectTeachersDonut,
1090
+ {
1091
+ total,
1092
+ approved,
1093
+ rejected,
1094
+ submitted,
1095
+ notSubmitted
1096
+ }
1097
+ ),
1098
+ /* @__PURE__ */ jsx25("span", { className: "basis-1/6 grow-0 shrink min-w-0" }),
1099
+ /* @__PURE__ */ jsx25(StatusLegend, { total, approved, rejected, submitted, notSubmitted })
1100
+ ] }, subjects.length)
1101
+ ] });
1102
+ }
1103
+
1104
+ // src/ui/basic/cookies-banner.tsx
1105
+ import { useEffect as useEffect8, useState as useState13 } from "react";
1106
+
1107
+ // ../node_modules/@sidekickicons/react/24/outline/esm/CookieIcon.js
1108
+ import * as React5 from "react";
1109
+ function CookieIcon({
1110
+ title,
1111
+ titleId,
1112
+ ...props
1113
+ }, svgRef) {
1114
+ return /* @__PURE__ */ React5.createElement("svg", Object.assign({
1115
+ xmlns: "http://www.w3.org/2000/svg",
1116
+ viewBox: "0 0 24 24",
1117
+ strokeWidth: 1.5,
1118
+ stroke: "currentColor",
1119
+ "aria-hidden": "true",
1120
+ "data-slot": "icon",
1121
+ fill: "none",
1122
+ ref: svgRef,
1123
+ "aria-labelledby": titleId
1124
+ }, props), title ? /* @__PURE__ */ React5.createElement("title", {
1125
+ id: titleId
1126
+ }, title) : null, /* @__PURE__ */ React5.createElement("path", {
1127
+ strokeLinecap: "round",
1128
+ strokeLinejoin: "round",
1129
+ d: "M11.602 3.009a9 9 0 1 0 9.389 9.389 2.75 2.75 0 0 1-4.6-1.03c-.409.24-.883.381-1.391.381a2.75 2.75 0 0 1-2.371-4.142 2.75 2.75 0 0 1-1.027-4.598zM19 9.499v0M18 6v0m-2.5 2.5v0m-1-3.5v0m-6 2.5v0m-2 7v0m3 1.51h1.25m-4.063-5.796 1.125 1m7.215 4.546.946-1M13 18.009v0m-2-5.01v0"
1130
+ }));
1131
+ }
1132
+ var ForwardRef = /* @__PURE__ */ React5.forwardRef(CookieIcon);
1133
+ var CookieIcon_default = ForwardRef;
1134
+
1135
+ // src/ui/basic/cookies-banner.tsx
1136
+ import { XMarkIcon } from "@heroicons/react/24/outline";
1137
+ import { COOKIES_DISMISSED_KEY, cookiesGet as cookiesGet2, cookiesSet as cookiesSet2 } from "@goralys/core";
1138
+ import { jsx as jsx26, jsxs as jsxs22 } from "react/jsx-runtime";
1139
+ function CookieBanner() {
1140
+ const Link = useGoralysUI().Link;
1141
+ const [visible, setVisible] = useState13(false);
1142
+ useEffect8(() => {
1143
+ if (!cookiesGet2(COOKIES_DISMISSED_KEY)) {
1144
+ setVisible(true);
1145
+ }
1146
+ }, []);
1147
+ if (!visible) return null;
1148
+ const discard = () => {
1149
+ cookiesSet2(COOKIES_DISMISSED_KEY, "1", "/", 60 * 60 * 24 * 365);
1150
+ setVisible(false);
1151
+ };
1152
+ return /* @__PURE__ */ jsxs22("div", { className: "fixed bottom-4 left-1/2 -translate-x-1/2 w-fit max-w-md z-10 bg-sky-200 rounded-md shadow-lg p-4 flex flex-col gap-2", children: [
1153
+ /* @__PURE__ */ jsxs22("div", { className: "flex items-center justify-between", children: [
1154
+ /* @__PURE__ */ jsxs22("p", { className: "font-semibold flex items-center gap-1", children: [
1155
+ /* @__PURE__ */ jsx26(CookieIcon_default, { className: "size-5" }),
1156
+ "Cookies"
1157
+ ] }),
1158
+ /* @__PURE__ */ jsx26(
1159
+ "button",
1160
+ {
1161
+ onClick: discard,
1162
+ className: "cursor-pointer hover:bg-sky-300 hover:shadow-md hover:-translate-y-1 rounded-md p-0.5\n transition-all ease-out duration-500",
1163
+ title: "Fermer",
1164
+ children: /* @__PURE__ */ jsx26(XMarkIcon, { className: "size-4" })
1165
+ }
1166
+ )
1167
+ ] }),
1168
+ /* @__PURE__ */ jsxs22("p", { className: "text-sm", children: [
1169
+ "Goralys utilise uniquement des cookies fonctionnels. Pour plus d'informations, consultez",
1170
+ " ",
1171
+ /* @__PURE__ */ jsx26(Link, { href: "/cookies", className: "text-sky-600 underline hover:text-sky-800 transition-colors duration-200", children: "cette page" })
1172
+ ] })
1173
+ ] });
1174
+ }
1175
+
1176
+ // src/ui/basic/goralys-logo-loader.tsx
1177
+ import { useEffect as useEffect9, useRef as useRef3 } from "react";
1178
+ import { LOGO_PATH } from "@goralys/core";
1179
+ import { jsx as jsx27, jsxs as jsxs23 } from "react/jsx-runtime";
1180
+ function GoralysLogoLoader() {
1181
+ const mainPathRef = useRef3(null);
1182
+ const eyePathRef = useRef3(null);
1183
+ const mainColorRef = useRef3(null);
1184
+ const eyeColorRef = useRef3(null);
1185
+ useEffect9(() => {
1186
+ [mainPathRef, eyePathRef, mainColorRef, eyeColorRef].forEach((ref) => {
1187
+ const path = ref.current;
1188
+ if (!path) return;
1189
+ const len = path.getTotalLength();
1190
+ path.style.strokeDasharray = String(len);
1191
+ path.style.setProperty("--path-length", String(len));
1192
+ path.style.strokeDashoffset = String(len);
1193
+ });
1194
+ }, []);
1195
+ return /* @__PURE__ */ jsxs23("div", { className: "flex items-center justify-center rounded-xl bg-sky-500 p-4 w-fit mx-auto", children: [
1196
+ /* @__PURE__ */ jsx27("style", { children: `
1197
+ .chameleon-path {
1198
+ fill: none;
1199
+ stroke-linecap: round;
1200
+ stroke-linejoin: round;
1201
+ }
1202
+
1203
+ .chameleon-main {
1204
+ stroke: #ffffff;
1205
+ stroke-width: 4;
1206
+ animation: chameleonDraw 9s cubic-bezier(0.37, 0, 0.63, 1) infinite;
1207
+ }
1208
+
1209
+ .chameleon-eye {
1210
+ stroke: #ffffff;
1211
+ stroke-width: 4;
1212
+ animation: chameleonDraw 9s cubic-bezier(0.37, 0, 0.63, 1) infinite;
1213
+ animation-delay: 0.3s;
1214
+ }
1215
+
1216
+ @keyframes chameleonDraw {
1217
+ 0% { stroke-dashoffset: var(--path-length); opacity: 0; }
1218
+ 3% { opacity: 1; }
1219
+ 50% { stroke-dashoffset: 0; opacity: 1; }
1220
+ 62% { stroke-dashoffset: 0; opacity: 1; }
1221
+ 80% { opacity: 0.6; }
1222
+ 97% { opacity: 0; }
1223
+ 100% { stroke-dashoffset: var(--path-length); opacity: 0; }
1224
+ }
1225
+ ` }),
1226
+ /* @__PURE__ */ jsxs23("svg", { viewBox: "345 350 305 315", fill: "none", "aria-label": "Chargement\u2026", role: "img", className: "block w-56 h-56", children: [
1227
+ /* @__PURE__ */ jsx27("path", { ref: mainPathRef, className: "chameleon-path chameleon-main", d: LOGO_PATH.main }),
1228
+ /* @__PURE__ */ jsx27("path", { ref: eyePathRef, className: "chameleon-path chameleon-eye", d: LOGO_PATH.eye })
1229
+ ] })
1230
+ ] });
1231
+ }
1232
+
1233
+ // src/ui/inputs/checkbox.tsx
1234
+ import { useId } from "react";
1235
+ import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
1236
+ function Checkbox({ id, label, setValueAction, defaultValue, className, disabled = false }) {
1237
+ const generatedId = useId();
1238
+ const checkBoxId = id || generatedId;
1239
+ return /* @__PURE__ */ jsxs24("div", { className: `flex items-center gap-0.5 ${className ?? ""}`, children: [
1240
+ /* @__PURE__ */ jsxs24(
1241
+ "label",
1242
+ {
1243
+ className: `relative flex items-center justify-center rounded-full p-1 ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`,
1244
+ htmlFor: checkBoxId,
1245
+ "data-ripple-dark": "true",
1246
+ children: [
1247
+ /* @__PURE__ */ jsx28(
1248
+ "input",
1249
+ {
1250
+ disabled,
1251
+ defaultChecked: defaultValue,
1252
+ onChange: (e) => setValueAction(e.target.checked),
1253
+ id: checkBoxId,
1254
+ type: "checkbox",
1255
+ className: "peer h-4 w-4 appearance-none rounded border border-sky-400\n bg-white shadow hover:shadow-md transition-all\n before:absolute before:top-2/4 before:left-2/4 before:block\n before:h-5 before:w-5 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full\n before:bg-sky-500 before:opacity-0 before:transition-opacity\n checked:border-sky-600 checked:bg-sky-600\n checked:before:bg-sky-500 hover:before:opacity-10\n cursor-pointer disabled:cursor-not-allowed"
1256
+ }
1257
+ ),
1258
+ /* @__PURE__ */ jsx28(
1259
+ "span",
1260
+ {
1261
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center\n text-white opacity-0 transition-opacity peer-checked:opacity-100",
1262
+ children: /* @__PURE__ */ jsx28(
1263
+ "svg",
1264
+ {
1265
+ xmlns: "http://www.w3.org/2000/svg",
1266
+ className: "h-3.5 w-3.5",
1267
+ viewBox: "0 0 20 20",
1268
+ fill: "currentColor",
1269
+ stroke: "currentColor",
1270
+ strokeWidth: "1",
1271
+ children: /* @__PURE__ */ jsx28(
1272
+ "path",
1273
+ {
1274
+ fillRule: "evenodd",
1275
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8\n 12.586l7.293-7.293a1 1 0 011.414 0z",
1276
+ clipRule: "evenodd"
1277
+ }
1278
+ )
1279
+ }
1280
+ )
1281
+ }
1282
+ )
1283
+ ]
1284
+ }
1285
+ ),
1286
+ /* @__PURE__ */ jsx28("label", { className: "text-black text-sm cursor-pointer", htmlFor: checkBoxId, children: label })
1287
+ ] });
1288
+ }
1289
+
1290
+ // src/ui/inputs/input-text-file.tsx
1291
+ import { useState as useState14 } from "react";
1292
+ import { ArrowUpTrayIcon } from "@heroicons/react/24/outline";
1293
+ import clsx6 from "clsx";
1294
+ import { jsx as jsx29, jsxs as jsxs25 } from "react/jsx-runtime";
1295
+ function InputTextFile({ text, maxSizeKB, onFileSelected }) {
1296
+ const [fileName, setFileName] = useState14(null);
1297
+ const [dragging, setDragging] = useState14(false);
1298
+ const toast = useToast();
1299
+ const handleFile = (file) => {
1300
+ if (!file) {
1301
+ toast.showToast({
1302
+ type: "warning",
1303
+ title: "Fichier",
1304
+ message: "Merci de fournir un fichier"
1305
+ });
1306
+ return;
1307
+ }
1308
+ if (!(file.name.endsWith(".txt") || file.name.endsWith(".odt") || file.name.endsWith(".docx"))) {
1309
+ toast.showToast({
1310
+ type: "warning",
1311
+ title: "Fichier",
1312
+ message: "Merci de fournir un fichier zip (.zip)"
1313
+ });
1314
+ return;
1315
+ }
1316
+ setFileName(file.name || null);
1317
+ onFileSelected(file);
1318
+ return;
1319
+ };
1320
+ return /* @__PURE__ */ jsxs25(
1321
+ "label",
1322
+ {
1323
+ htmlFor: "doc",
1324
+ className: clsx6("flex items-center gap-0 rounded-xs border border-sky-400 border-dashed bg-sky-200 cursor-pointer", {
1325
+ "bg-sky-300": dragging
1326
+ }),
1327
+ onDragOver: (e) => {
1328
+ e.preventDefault();
1329
+ setDragging(true);
1330
+ },
1331
+ onDragLeave: () => setDragging(false),
1332
+ onDrop: (e) => {
1333
+ e.preventDefault();
1334
+ setDragging(false);
1335
+ handleFile(e.dataTransfer?.files[0]);
1336
+ },
1337
+ children: [
1338
+ /* @__PURE__ */ jsx29(ArrowUpTrayIcon, { className: "size-7 ml-1 mr-2" }),
1339
+ /* @__PURE__ */ jsxs25("div", { className: "", children: [
1340
+ /* @__PURE__ */ jsx29("h4", { className: "text-base font-semibold text-gray-700", children: fileName || text }),
1341
+ /* @__PURE__ */ jsxs25("span", { className: "text-sm text-gray-500", children: [
1342
+ "Max ",
1343
+ maxSizeKB,
1344
+ " KO (.txt, .odt, .docx)"
1345
+ ] })
1346
+ ] }),
1347
+ /* @__PURE__ */ jsx29(
1348
+ "input",
1349
+ {
1350
+ type: "file",
1351
+ id: "doc",
1352
+ name: "doc",
1353
+ accept: ".txt,.odt,.docx",
1354
+ hidden: true,
1355
+ onChange: (e) => handleFile(e.target.files?.[0])
1356
+ },
1357
+ `input-file-input-${text}-max-size-${maxSizeKB}-kb`
1358
+ )
1359
+ ]
1360
+ },
1361
+ `input-file-label-${text}-max-size-${maxSizeKB}-kb`
1362
+ );
1363
+ }
1364
+
1365
+ // src/ui/inputs/input-zip-file.tsx
1366
+ import { useState as useState15 } from "react";
1367
+ import { ArrowUpTrayIcon as ArrowUpTrayIcon2 } from "@heroicons/react/24/outline";
1368
+ import clsx7 from "clsx";
1369
+ import { jsx as jsx30, jsxs as jsxs26 } from "react/jsx-runtime";
1370
+ function InputZipFile({ text, onFileSelected }) {
1371
+ const [fileName, setFileName] = useState15(null);
1372
+ const [dragging, setDragging] = useState15(false);
1373
+ const toast = useToast();
1374
+ const handleFile = (file) => {
1375
+ if (!file) {
1376
+ toast.showToast({
1377
+ type: "warning",
1378
+ title: "Fichier",
1379
+ message: "Merci de fournir un fichier"
1380
+ });
1381
+ return;
1382
+ }
1383
+ if (!file.name.endsWith(".zip")) {
1384
+ toast.showToast({
1385
+ type: "warning",
1386
+ title: "Fichier",
1387
+ message: "Merci de fournir un fichier zip (.zip)"
1388
+ });
1389
+ return;
1390
+ }
1391
+ setFileName(file.name || null);
1392
+ onFileSelected(file);
1393
+ return;
1394
+ };
1395
+ return /* @__PURE__ */ jsxs26(
1396
+ "label",
1397
+ {
1398
+ htmlFor: "doc",
1399
+ className: clsx7("flex items-center gap-0 rounded-xs border border-sky-400 border-dashed bg-sky-200 cursor-pointer", {
1400
+ "bg-sky-300": dragging
1401
+ }),
1402
+ onDragOver: (e) => {
1403
+ e.preventDefault();
1404
+ setDragging(true);
1405
+ },
1406
+ onDragLeave: () => setDragging(false),
1407
+ onDrop: (e) => {
1408
+ e.preventDefault();
1409
+ setDragging(false);
1410
+ handleFile(e.dataTransfer?.files[0]);
1411
+ },
1412
+ children: [
1413
+ /* @__PURE__ */ jsx30(ArrowUpTrayIcon2, { className: "size-7 ml-1 mr-2" }),
1414
+ /* @__PURE__ */ jsxs26("div", { className: "", children: [
1415
+ /* @__PURE__ */ jsx30("h4", { className: "text-base font-semibold text-gray-700", children: fileName || text }),
1416
+ /* @__PURE__ */ jsx30("span", { className: "text-sm text-gray-500", children: "(.zip)" })
1417
+ ] }),
1418
+ /* @__PURE__ */ jsx30(
1419
+ "input",
1420
+ {
1421
+ type: "file",
1422
+ id: "doc",
1423
+ name: "doc",
1424
+ accept: ".zip",
1425
+ hidden: true,
1426
+ onChange: (e) => handleFile(e.target.files?.[0])
1427
+ },
1428
+ `input-file-input-${text}`
1429
+ )
1430
+ ]
1431
+ },
1432
+ `input-file-label-${text}`
1433
+ );
1434
+ }
1435
+
1436
+ // src/ui/inputs/subject-input-admin.tsx
1437
+ import { getStatusHelper } from "@goralys/core";
1438
+
1439
+ // src/ui/inputs/subject-text-area.tsx
1440
+ import { clsx as clsx8 } from "clsx";
1441
+ import { useState as useState16 } from "react";
1442
+ import { jsx as jsx31, jsxs as jsxs27 } from "react/jsx-runtime";
1443
+ function SubjectTextArea({
1444
+ id,
1445
+ label,
1446
+ helper,
1447
+ disabled = false,
1448
+ ref,
1449
+ defaultValue,
1450
+ maxLength,
1451
+ onChangeAction,
1452
+ subjectData,
1453
+ animate
1454
+ }) {
1455
+ const setRef = useAutoResize(ref);
1456
+ const [currentValue, setCurrentValue] = useState16(defaultValue ?? "");
1457
+ const update = (e) => {
1458
+ if (maxLength && e.target.value.length > maxLength) {
1459
+ return;
1460
+ }
1461
+ setCurrentValue(e.currentTarget.value);
1462
+ if (onChangeAction) onChangeAction(e);
1463
+ };
1464
+ return /* @__PURE__ */ jsxs27("div", { className: "relative mt-0 mb-0 group min-w-50 w-full", children: [
1465
+ /* @__PURE__ */ jsxs27("div", { className: "relative", children: [
1466
+ /* @__PURE__ */ jsx31(
1467
+ "textarea",
1468
+ {
1469
+ ref: setRef,
1470
+ id,
1471
+ name: id,
1472
+ rows: 1,
1473
+ placeholder: " ",
1474
+ spellCheck: "true",
1475
+ defaultValue,
1476
+ readOnly: disabled,
1477
+ maxLength,
1478
+ onChange: update,
1479
+ className: clsx8(
1480
+ "peer block w-full py-0 px-0 cursor-text text-base leading-5 bg-transparent border-0 border-b-2 border-sky-300 appearance-none focus:outline-none focus:ring-0 resize-none overflow-hidden ",
1481
+ {
1482
+ "border-green-600!": subjectData.status === "approved",
1483
+ "border-amber-600!": subjectData.status === "submitted",
1484
+ "border-red-600!": subjectData.status === "rejected"
1485
+ },
1486
+ {
1487
+ "text-gray-700 cursor-not-allowed!": disabled
1488
+ }
1489
+ )
1490
+ }
1491
+ ),
1492
+ animate && /* @__PURE__ */ jsx31(
1493
+ "span",
1494
+ {
1495
+ className: "pointer-events-none\n absolute bg-sky-500 left-0 bottom-0 h-0.5 w-full\n origin-left scale-x-0\n transition-transform duration-250\n group-focus-within:scale-x-100 "
1496
+ }
1497
+ ),
1498
+ /* @__PURE__ */ jsx31(
1499
+ "label",
1500
+ {
1501
+ htmlFor: id,
1502
+ className: clsx8(
1503
+ "absolute text-base text-body cursor-text duration-300 transform -translate-y-4.5 scale-75 top-0 origin-left peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4.5",
1504
+ { "cursor-not-allowed": disabled }
1505
+ ),
1506
+ children: label
1507
+ }
1508
+ )
1509
+ ] }),
1510
+ /* @__PURE__ */ jsx31("div", { className: "flex flex-row content-between w-full", children: /* @__PURE__ */ jsxs27("div", { className: "flex flex-col", children: [
1511
+ maxLength && /* @__PURE__ */ jsxs27(
1512
+ "p",
1513
+ {
1514
+ className: clsx8("mt-1 mb-0 p-0 relative text-[11px] italic", {
1515
+ "text-gray-600": currentValue.length < maxLength * 0.9,
1516
+ "text-amber-600": currentValue.length >= maxLength * 0.9 && maxLength > currentValue.length,
1517
+ "text-red-600": currentValue.length >= maxLength
1518
+ }),
1519
+ children: [
1520
+ currentValue.length,
1521
+ "/",
1522
+ maxLength,
1523
+ " caract\xE8res"
1524
+ ]
1525
+ }
1526
+ ),
1527
+ helper && helper.length !== 0 && /* @__PURE__ */ jsxs27(
1528
+ "p",
1529
+ {
1530
+ className: clsx8("self-center relative text-[13px] italic text-gray-600", {
1531
+ "mt-1": maxLength === void 0
1532
+ }),
1533
+ children: [
1534
+ "*",
1535
+ helper
1536
+ ]
1537
+ }
1538
+ )
1539
+ ] }) })
1540
+ ] });
1541
+ }
1542
+
1543
+ // src/ui/inputs/subject-input-admin.tsx
1544
+ import { jsx as jsx32 } from "react/jsx-runtime";
1545
+ function SubjectInputAdmin({ id, label, helper, subjectData, onChangeAction }) {
1546
+ helper = getStatusHelper(subjectData.status, "admin");
1547
+ return /* @__PURE__ */ jsx32("div", { className: "relative mt-2 group min-w-50", children: /* @__PURE__ */ jsx32(
1548
+ SubjectTextArea,
1549
+ {
1550
+ id,
1551
+ disabled: true,
1552
+ onChangeAction,
1553
+ label,
1554
+ animate: false,
1555
+ subjectData,
1556
+ defaultValue: subjectData.subject,
1557
+ helper
1558
+ }
1559
+ ) });
1560
+ }
1561
+
1562
+ // src/ui/inputs/subject-input-student.tsx
1563
+ import { getStatusHelper as getStatusHelper2 } from "@goralys/core";
1564
+ import { jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
1565
+ function SubjectInputStudent({
1566
+ id,
1567
+ label,
1568
+ helper,
1569
+ subjectData,
1570
+ setIsInterdisciplinaryAction = () => {
1571
+ },
1572
+ onChangeAction
1573
+ }) {
1574
+ const initialValue = subjectData.status == "rejected" ? subjectData.lastRejected ?? "" : subjectData.subject ?? "";
1575
+ const MAX_CHARS = 250;
1576
+ helper = getStatusHelper2(subjectData.status, "student");
1577
+ const editable = subjectData.status != "approved" && subjectData.status != "submitted";
1578
+ const handleOnChange = (e) => {
1579
+ if (onChangeAction) {
1580
+ onChangeAction(e);
1581
+ }
1582
+ };
1583
+ return /* @__PURE__ */ jsxs28("div", { className: "relative mt-2 group min-w-50 mb-0 flex sm:block flex-col", children: [
1584
+ /* @__PURE__ */ jsx33(
1585
+ SubjectTextArea,
1586
+ {
1587
+ id,
1588
+ disabled: !editable,
1589
+ defaultValue: initialValue,
1590
+ maxLength: MAX_CHARS,
1591
+ onChangeAction: handleOnChange,
1592
+ label,
1593
+ subjectData,
1594
+ animate: editable,
1595
+ helper
1596
+ }
1597
+ ),
1598
+ /* @__PURE__ */ jsx33("div", { className: "flex flex-row content-between w-full", children: /* @__PURE__ */ jsx33(
1599
+ Checkbox,
1600
+ {
1601
+ className: "m-0 sm:-mt-7.5 sm:ml-auto self-center",
1602
+ label: "Question transversale",
1603
+ setValueAction: setIsInterdisciplinaryAction,
1604
+ defaultValue: subjectData.interdisciplinary,
1605
+ disabled: !editable
1606
+ }
1607
+ ) })
1608
+ ] });
1609
+ }
1610
+
1611
+ // src/ui/inputs/subject-input-teacher.tsx
1612
+ import { buildApiUrl as buildApiUrl2, getGoralysClientConfig, getStatusHelper as getStatusHelper3 } from "@goralys/core";
1613
+ import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
1614
+ import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
1615
+ function SubjectInputTeacher({ id, label, helper, subjectData, onChangeAction }) {
1616
+ helper = getStatusHelper3(subjectData.status, "teacher");
1617
+ const initialValue = subjectData.status == "rejected" ? subjectData.lastRejected ?? "" : subjectData.subject ?? "";
1618
+ const MAX_CHARS = 250;
1619
+ const handleOnChange = (e) => {
1620
+ if (onChangeAction) {
1621
+ onChangeAction(e);
1622
+ }
1623
+ };
1624
+ const getDraft = () => {
1625
+ const cfg = getGoralysClientConfig();
1626
+ window.location.href = buildApiUrl2(
1627
+ "subjects/draft",
1628
+ {
1629
+ "high-school-token": cfg.getSchoolToken() ?? null,
1630
+ teacher: subjectData.teacherToken,
1631
+ student: subjectData.studentToken,
1632
+ topic: subjectData.topicCode,
1633
+ "file-name": `Brouillon - ${subjectData.student} ${subjectData.topicCode}`
1634
+ },
1635
+ true
1636
+ );
1637
+ };
1638
+ return /* @__PURE__ */ jsxs29("div", { className: "relative mt-2 group min-w-50 mb-0 flex sm:block flex-col", children: [
1639
+ /* @__PURE__ */ jsxs29("div", { className: "flex flex-row", children: [
1640
+ /* @__PURE__ */ jsx34(
1641
+ SubjectTextArea,
1642
+ {
1643
+ id,
1644
+ disabled: true,
1645
+ defaultValue: initialValue,
1646
+ maxLength: MAX_CHARS,
1647
+ onChangeAction: handleOnChange,
1648
+ label,
1649
+ subjectData,
1650
+ animate: false,
1651
+ helper
1652
+ }
1653
+ ),
1654
+ subjectData.hasDraft && /* @__PURE__ */ jsx34(
1655
+ "button",
1656
+ {
1657
+ className: "h-6 w-6 cursor-pointer bg-sky-200 rounded-md items-center justify-center\n hover:bg-sky-300 hover:shadow-md hover:-translate-y-1 transition-all ease-out duration-500",
1658
+ type: "submit",
1659
+ title: "T\xE9l\xE9charger le brouillon de l'\xE9l\xE8ve",
1660
+ onClick: getDraft,
1661
+ children: /* @__PURE__ */ jsx34(ArrowDownTrayIcon, { className: "size-5 m-auto" })
1662
+ }
1663
+ )
1664
+ ] }),
1665
+ /* @__PURE__ */ jsx34("div", { className: "flex flex-row content-between w-full", children: /* @__PURE__ */ jsx34(
1666
+ Checkbox,
1667
+ {
1668
+ id: `interdisciplinary-teacher-${subjectData.studentToken}-${subjectData.teacherToken}`,
1669
+ className: "m-0 sm:ml-auto sm:-mt-7.5 mb-2.5 self-center",
1670
+ label: "Question transversale",
1671
+ setValueAction: () => {
1672
+ },
1673
+ defaultValue: subjectData.interdisciplinary,
1674
+ disabled: true
1675
+ }
1676
+ ) })
1677
+ ] });
1678
+ }
1679
+
1680
+ // src/ui/inputs/text-area.tsx
1681
+ import { clsx as clsx9 } from "clsx";
1682
+ import { useState as useState17 } from "react";
1683
+ import { jsx as jsx35, jsxs as jsxs30 } from "react/jsx-runtime";
1684
+ function TextArea({
1685
+ id,
1686
+ label,
1687
+ helper,
1688
+ required = false,
1689
+ disabled = false,
1690
+ ref,
1691
+ defaultValue,
1692
+ onChangeAction,
1693
+ maxLength
1694
+ }) {
1695
+ const setRef = useAutoResize(ref);
1696
+ const [currentValue, setCurrentValue] = useState17(defaultValue ?? "");
1697
+ const update = (e) => {
1698
+ if (maxLength && e.target.value.length > maxLength) {
1699
+ return;
1700
+ }
1701
+ setCurrentValue(e.currentTarget.value);
1702
+ if (onChangeAction) onChangeAction(e);
1703
+ };
1704
+ return /* @__PURE__ */ jsxs30("div", { className: "relative mt-3 mb-1 group min-w-50", children: [
1705
+ /* @__PURE__ */ jsxs30("div", { className: "relative", children: [
1706
+ /* @__PURE__ */ jsx35(
1707
+ "textarea",
1708
+ {
1709
+ ref: setRef,
1710
+ id,
1711
+ name: id,
1712
+ rows: 1,
1713
+ placeholder: " ",
1714
+ spellCheck: "true",
1715
+ defaultValue,
1716
+ maxLength,
1717
+ readOnly: disabled,
1718
+ onChange: update,
1719
+ required,
1720
+ className: "\n peer block w-full py-0 px-0 text-base text-heading\n resize-none overflow-hidden bg-transparent\n border-0 border-b-2 border-sky-300\n appearance-none focus:outline-none focus:ring-0\n "
1721
+ }
1722
+ ),
1723
+ /* @__PURE__ */ jsx35(
1724
+ "span",
1725
+ {
1726
+ className: "pointer-events-none\n absolute bg-sky-500 left-0 bottom-0 h-0.5 w-full\n origin-left scale-x-0\n transition-transform duration-250\n group-focus-within:scale-x-100 "
1727
+ }
1728
+ ),
1729
+ /* @__PURE__ */ jsx35(
1730
+ "label",
1731
+ {
1732
+ htmlFor: id,
1733
+ className: clsx9(
1734
+ "absolute text-base text-body cursor-text duration-300 transform -translate-y-4.5 scale-75 top-0 origin-left peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4.5",
1735
+ { "cursor-not-allowed": disabled }
1736
+ ),
1737
+ children: label
1738
+ }
1739
+ )
1740
+ ] }),
1741
+ /* @__PURE__ */ jsx35("div", { className: "flex flex-row content-between w-full", children: /* @__PURE__ */ jsxs30("div", { className: "flex flex-col", children: [
1742
+ maxLength && /* @__PURE__ */ jsxs30(
1743
+ "p",
1744
+ {
1745
+ className: clsx9("mt-1 mb-0 p-0 relative text-[11px] italic", {
1746
+ "text-gray-600": currentValue.length < maxLength * 0.9,
1747
+ "text-amber-600": currentValue.length >= maxLength * 0.9 && maxLength > currentValue.length,
1748
+ "text-red-600": currentValue.length >= maxLength
1749
+ }),
1750
+ children: [
1751
+ currentValue.length,
1752
+ "/",
1753
+ maxLength,
1754
+ " caract\xE8res"
1755
+ ]
1756
+ }
1757
+ ),
1758
+ helper && helper.length !== 0 && /* @__PURE__ */ jsxs30(
1759
+ "p",
1760
+ {
1761
+ className: clsx9("self-center relative text-[13px] italic text-gray-600", {
1762
+ "mt-1": maxLength === void 0
1763
+ }),
1764
+ children: [
1765
+ "*",
1766
+ helper
1767
+ ]
1768
+ }
1769
+ )
1770
+ ] }) })
1771
+ ] });
1772
+ }
1773
+
1774
+ // src/ui/modals/drafts/draft-modal-provider.tsx
1775
+ import { createContext as createContext5, useCallback as useCallback5, useContext as useContext5, useMemo as useMemo5, useState as useState19 } from "react";
1776
+
1777
+ // src/ui/modals/drafts/draft-modal-element.tsx
1778
+ import { clsx as clsx10 } from "clsx";
1779
+ import { useRef as useRef4, useState as useState18 } from "react";
1780
+ import { QuestionMarkCircleIcon as QuestionMarkCircleIcon2 } from "@heroicons/react/24/outline";
1781
+ import { jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
1782
+ function DraftModalElement({ visible, onChooseDraftAction, onCancelAction, onCloseModalAction }) {
1783
+ const [draftFile, setDraftFile] = useState18(null);
1784
+ const modalRef = useRef4(null);
1785
+ useModalClose(modalRef, visible, onCloseModalAction);
1786
+ const onConfirm = () => {
1787
+ onChooseDraftAction(draftFile);
1788
+ };
1789
+ return /* @__PURE__ */ jsxs31(
1790
+ "div",
1791
+ {
1792
+ ref: modalRef,
1793
+ className: clsx10(
1794
+ "fixed flex flex-col gap-2 p-3 w-125 bg-sky-200 rounded shadow overflow-hidden left-1/2 -translate-x-1/2 top-1 ",
1795
+ "after:absolute after:left-0 after:top-0 after:h-full after:w-1.25 after:content-[''] after:bg-blue-500",
1796
+ "transition-all duration-500 z-50 ",
1797
+ {
1798
+ "translate-y-0 opacity-100": visible,
1799
+ "-translate-y-5 opacity-0": !visible
1800
+ }
1801
+ ),
1802
+ role: "dialog",
1803
+ "aria-modal": "true",
1804
+ children: [
1805
+ /* @__PURE__ */ jsxs31("div", { className: "flex gap-3", children: [
1806
+ /* @__PURE__ */ jsx36("div", { className: "w-11 h-11 flex self-center items-center justify-center", children: /* @__PURE__ */ jsx36(QuestionMarkCircleIcon2, { className: "size-15 text-blue-500" }) }),
1807
+ /* @__PURE__ */ jsxs31("div", { className: "flex flex-col justify-center flex-1", children: [
1808
+ /* @__PURE__ */ jsx36("strong", { className: "text-md", children: "Choix du brouillon" }),
1809
+ /* @__PURE__ */ jsx36("span", { className: "text-sm", children: "Vous pouvez joindre un brouillon \xE0 votre question avant de l'envoyer. Le professeur pourra ensuite le t\xE9l\xE9charger et le consulter." })
1810
+ ] })
1811
+ ] }),
1812
+ /* @__PURE__ */ jsx36(InputTextFile, { text: "T\xE9verser un brouillon", maxSizeKB: 50, onFileSelected: setDraftFile }),
1813
+ /* @__PURE__ */ jsxs31("div", { className: "flex justify-between gap-2 mt-2", children: [
1814
+ /* @__PURE__ */ jsx36(
1815
+ Button,
1816
+ {
1817
+ className: "bg-gray-400! before:bg-gray-500! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
1818
+ text: "Envoyer la question seule",
1819
+ type: "button",
1820
+ onClick: onCancelAction
1821
+ }
1822
+ ),
1823
+ /* @__PURE__ */ jsx36(
1824
+ Button,
1825
+ {
1826
+ className: "bg-blue-500! before:bg-blue-600! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
1827
+ text: "Envoyer le brouillon",
1828
+ type: "button",
1829
+ onClick: onConfirm
1830
+ }
1831
+ )
1832
+ ] })
1833
+ ]
1834
+ }
1835
+ );
1836
+ }
1837
+
1838
+ // src/ui/modals/drafts/draft-modal-provider.tsx
1839
+ import { createPortal as createPortal4 } from "react-dom";
1840
+ import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
1841
+ var DraftModalContext = createContext5(null);
1842
+ function DraftModalProvider({ children }) {
1843
+ const [fileChosen, setChosenFile] = useState19(null);
1844
+ const [visible, setVisible] = useState19(false);
1845
+ const showDraftModal = useCallback5(() => {
1846
+ return new Promise((resolve) => {
1847
+ setChosenFile({ resolve });
1848
+ setVisible(false);
1849
+ requestAnimationFrame(() => setVisible(true));
1850
+ });
1851
+ }, []);
1852
+ function handleChooseDraft(file) {
1853
+ setVisible(false);
1854
+ setTimeout(() => {
1855
+ fileChosen?.resolve({ type: "withDraft", file });
1856
+ setChosenFile(null);
1857
+ }, 500);
1858
+ }
1859
+ function handleCancel() {
1860
+ setVisible(false);
1861
+ setTimeout(() => {
1862
+ fileChosen?.resolve({ type: "withoutDraft" });
1863
+ setChosenFile(null);
1864
+ }, 500);
1865
+ }
1866
+ function handleClose() {
1867
+ setVisible(false);
1868
+ setTimeout(() => {
1869
+ fileChosen?.resolve({ type: "closed" });
1870
+ setChosenFile(null);
1871
+ }, 500);
1872
+ }
1873
+ const value = useMemo5(() => ({ showDraftModal }), [showDraftModal]);
1874
+ return /* @__PURE__ */ jsxs32(DraftModalContext.Provider, { value, children: [
1875
+ children,
1876
+ fileChosen && typeof document !== "undefined" && createPortal4(
1877
+ /* @__PURE__ */ jsx37("div", { className: "fixed inset-0 z-40 bg-black/20 backdrop-blur-sm", children: /* @__PURE__ */ jsx37(
1878
+ DraftModalElement,
1879
+ {
1880
+ visible,
1881
+ onChooseDraftAction: handleChooseDraft,
1882
+ onCancelAction: handleCancel,
1883
+ onCloseModalAction: handleClose
1884
+ }
1885
+ ) }),
1886
+ document.getElementById("draft-modal-root")
1887
+ )
1888
+ ] });
1889
+ }
1890
+ function useDraftModal() {
1891
+ const context = useContext5(DraftModalContext);
1892
+ if (!context) {
1893
+ throw new Error("useDraftModal must be used within a ConfirmProvider");
1894
+ }
1895
+ return context;
1896
+ }
1897
+
1898
+ // src/ui/modals/import-topics/import-topics-modal-provider.tsx
1899
+ import { createContext as createContext6, useCallback as useCallback6, useContext as useContext6, useMemo as useMemo6, useState as useState21 } from "react";
1900
+
1901
+ // src/ui/modals/import-topics/import-topics-modal-element.tsx
1902
+ import { clsx as clsx11 } from "clsx";
1903
+ import { useRef as useRef5, useState as useState20 } from "react";
1904
+ import { QuestionMarkCircleIcon as QuestionMarkCircleIcon3 } from "@heroicons/react/24/outline";
1905
+ import { jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
1906
+ function ImportTopicsModalElement({ visible, onImportTopicsAction, onCancelAction, onCloseModalAction }) {
1907
+ const [topicsFile, setTopicsFile] = useState20(null);
1908
+ const modalRef = useRef5(null);
1909
+ useModalClose(modalRef, visible, onCloseModalAction);
1910
+ const onConfirm = () => {
1911
+ onImportTopicsAction(topicsFile);
1912
+ };
1913
+ return /* @__PURE__ */ jsxs33(
1914
+ "div",
1915
+ {
1916
+ ref: modalRef,
1917
+ className: clsx11(
1918
+ "fixed flex flex-col gap-2 p-3 w-125 bg-sky-200 rounded shadow overflow-hidden left-1/2 -translate-x-1/2 top-1 ",
1919
+ "after:absolute after:left-0 after:top-0 after:h-full after:w-1.25 after:content-[''] after:bg-blue-500",
1920
+ "transition-all duration-500 z-50 ",
1921
+ {
1922
+ "translate-y-0 opacity-100": visible,
1923
+ "-translate-y-5 opacity-0": !visible
1924
+ }
1925
+ ),
1926
+ role: "dialog",
1927
+ "aria-modal": "true",
1928
+ children: [
1929
+ /* @__PURE__ */ jsxs33("div", { className: "flex gap-3", children: [
1930
+ /* @__PURE__ */ jsx38("div", { className: "w-11 h-11 flex self-center items-center justify-center", children: /* @__PURE__ */ jsx38(QuestionMarkCircleIcon3, { className: "size-15 text-blue-500" }) }),
1931
+ /* @__PURE__ */ jsxs33("div", { className: "flex flex-col justify-center flex-1", children: [
1932
+ /* @__PURE__ */ jsx38("strong", { className: "text-md", children: "Import des donn\xE9es" }),
1933
+ /* @__PURE__ */ jsx38("span", { className: "text-sm", children: "Vous pouvez importer les donn\xE9es sous forme d'un fichiez zip" })
1934
+ ] })
1935
+ ] }),
1936
+ /* @__PURE__ */ jsx38(InputZipFile, { text: "T\xE9verser une archive", onFileSelected: setTopicsFile }),
1937
+ /* @__PURE__ */ jsxs33("div", { className: "flex justify-between gap-2 mt-2", children: [
1938
+ /* @__PURE__ */ jsx38(
1939
+ Button,
1940
+ {
1941
+ className: "bg-gray-400! before:bg-gray-500! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
1942
+ text: "Annuler",
1943
+ type: "button",
1944
+ onClick: onCancelAction
1945
+ }
1946
+ ),
1947
+ /* @__PURE__ */ jsx38(
1948
+ Button,
1949
+ {
1950
+ className: "bg-blue-500! before:bg-blue-600! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
1951
+ text: "Envoyer les donn\xE9es",
1952
+ type: "button",
1953
+ onClick: onConfirm
1954
+ }
1955
+ )
1956
+ ] })
1957
+ ]
1958
+ }
1959
+ );
1960
+ }
1961
+
1962
+ // src/ui/modals/import-topics/import-topics-modal-provider.tsx
1963
+ import { createPortal as createPortal5 } from "react-dom";
1964
+ import { jsx as jsx39, jsxs as jsxs34 } from "react/jsx-runtime";
1965
+ var ImportTopicsModalContext = createContext6(null);
1966
+ function ImportTopicsModalProvider({ children }) {
1967
+ const [fileChosen, setChosenFile] = useState21(null);
1968
+ const [visible, setVisible] = useState21(false);
1969
+ const showImportTopicsModal = useCallback6(() => {
1970
+ return new Promise((resolve) => {
1971
+ setChosenFile({ resolve });
1972
+ setVisible(false);
1973
+ requestAnimationFrame(() => setVisible(true));
1974
+ });
1975
+ }, []);
1976
+ function handleImportTopics(file) {
1977
+ setVisible(false);
1978
+ setTimeout(() => {
1979
+ fileChosen?.resolve(file);
1980
+ setChosenFile(null);
1981
+ }, 500);
1982
+ }
1983
+ function handleCancel() {
1984
+ setVisible(false);
1985
+ setTimeout(() => {
1986
+ fileChosen?.resolve(null);
1987
+ setChosenFile(null);
1988
+ }, 500);
1989
+ }
1990
+ function handleClose() {
1991
+ setVisible(false);
1992
+ setTimeout(() => {
1993
+ fileChosen?.resolve("modalClosed");
1994
+ setChosenFile(null);
1995
+ }, 500);
1996
+ }
1997
+ const value = useMemo6(() => ({ showImportTopicsModal }), [showImportTopicsModal]);
1998
+ return /* @__PURE__ */ jsxs34(ImportTopicsModalContext.Provider, { value, children: [
1999
+ children,
2000
+ fileChosen && typeof document !== "undefined" && createPortal5(
2001
+ /* @__PURE__ */ jsx39("div", { className: "fixed inset-0 z-40 bg-black/20 backdrop-blur-sm", children: /* @__PURE__ */ jsx39(
2002
+ ImportTopicsModalElement,
2003
+ {
2004
+ visible,
2005
+ onImportTopicsAction: handleImportTopics,
2006
+ onCancelAction: handleCancel,
2007
+ onCloseModalAction: handleClose
2008
+ }
2009
+ ) }),
2010
+ document.getElementById("import-topics-modal-root")
2011
+ )
2012
+ ] });
2013
+ }
2014
+ function useImportTopicsModal() {
2015
+ const context = useContext6(ImportTopicsModalContext);
2016
+ if (!context) {
2017
+ throw new Error("useImportTopicsModal must be used within a ConfirmProvider");
2018
+ }
2019
+ return context;
2020
+ }
2021
+
2022
+ // src/ui/modals/email/email-modal-provider.tsx
2023
+ import { createContext as createContext7, useCallback as useCallback7, useContext as useContext7, useMemo as useMemo7, useState as useState23 } from "react";
2024
+
2025
+ // src/ui/modals/email/email-modal-element.tsx
2026
+ import { clsx as clsx12 } from "clsx";
2027
+ import { useRef as useRef6, useState as useState22 } from "react";
2028
+ import { AtSymbolIcon } from "@heroicons/react/24/outline";
2029
+ import { jsx as jsx40, jsxs as jsxs35 } from "react/jsx-runtime";
2030
+ function EmailModalElement({ visible, onConfirmAction, onCancelAction, onCloseModalAction, message }) {
2031
+ const [email, setEmail] = useState22("");
2032
+ const modalRef = useRef6(null);
2033
+ useModalClose(modalRef, visible, onCloseModalAction);
2034
+ const onConfirm = () => {
2035
+ onConfirmAction(email);
2036
+ setEmail("");
2037
+ };
2038
+ const onCancel = () => {
2039
+ setEmail("");
2040
+ onCancelAction();
2041
+ };
2042
+ return /* @__PURE__ */ jsxs35(
2043
+ "div",
2044
+ {
2045
+ ref: modalRef,
2046
+ className: clsx12(
2047
+ "fixed flex flex-col gap-2 p-3 sm:w-125 w-96 bg-sky-200 rounded shadow overflow-hidden left-1/2 -translate-x-1/2 top-1",
2048
+ "after:absolute after:left-0 after:top-0 after:h-full after:w-1.25 after:content-[''] after:bg-blue-500",
2049
+ "transition-all duration-500 z-50",
2050
+ {
2051
+ "translate-y-0 opacity-100": visible,
2052
+ "-translate-y-5 opacity-0": !visible
2053
+ }
2054
+ ),
2055
+ role: "dialog",
2056
+ "aria-modal": "true",
2057
+ children: [
2058
+ /* @__PURE__ */ jsxs35("div", { className: "flex gap-3", children: [
2059
+ /* @__PURE__ */ jsx40("div", { className: "w-11 h-11 flex self-center items-center justify-center", children: /* @__PURE__ */ jsx40(AtSymbolIcon, { className: "size-15 text-blue-500" }) }),
2060
+ /* @__PURE__ */ jsxs35("div", { className: "flex flex-col justify-center flex-1", children: [
2061
+ /* @__PURE__ */ jsx40("strong", { className: "text-md", children: "Confirmation requise" }),
2062
+ /* @__PURE__ */ jsx40("span", { className: "text-sm", children: message })
2063
+ ] })
2064
+ ] }),
2065
+ /* @__PURE__ */ jsx40(
2066
+ FloatingInput,
2067
+ {
2068
+ id: "confirm-email",
2069
+ label: "Adresse mail",
2070
+ email: true,
2071
+ autocomplete: "email",
2072
+ onInput: (e) => setEmail(e.currentTarget.value)
2073
+ }
2074
+ ),
2075
+ /* @__PURE__ */ jsxs35("div", { className: "flex justify-between gap-2 mt-2", children: [
2076
+ /* @__PURE__ */ jsx40(
2077
+ Button,
2078
+ {
2079
+ className: "bg-gray-400! before:bg-gray-500! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
2080
+ text: "Annuler",
2081
+ type: "button",
2082
+ onClick: onCancel
2083
+ }
2084
+ ),
2085
+ /* @__PURE__ */ jsx40(
2086
+ Button,
2087
+ {
2088
+ className: "bg-blue-500! before:bg-blue-600! text-white! border-none! shadow-none! mt-0! mb-0! h-11! w-50!",
2089
+ text: "Confirmer",
2090
+ type: "button",
2091
+ onClick: onConfirm
2092
+ }
2093
+ )
2094
+ ] })
2095
+ ]
2096
+ }
2097
+ );
2098
+ }
2099
+
2100
+ // src/ui/modals/email/email-modal-provider.tsx
2101
+ import { createPortal as createPortal6 } from "react-dom";
2102
+ import { jsx as jsx41, jsxs as jsxs36 } from "react/jsx-runtime";
2103
+ var EmailModalContext = createContext7(null);
2104
+ function EmailModalProvider({ children }) {
2105
+ const [state, setState] = useState23(null);
2106
+ const [visible, setVisible] = useState23(false);
2107
+ const [message, setMessage] = useState23("");
2108
+ const DEFAULT_EMAIL_CONFIRM = "Veuillez entrer nouvel email pour confirmer.";
2109
+ const showEmailModal = useCallback7((message2 = DEFAULT_EMAIL_CONFIRM) => {
2110
+ setMessage(message2);
2111
+ return new Promise((resolve) => {
2112
+ setState({ resolve });
2113
+ setVisible(false);
2114
+ requestAnimationFrame(() => setVisible(true));
2115
+ });
2116
+ }, []);
2117
+ function handleConfirm(email) {
2118
+ setVisible(false);
2119
+ setTimeout(() => {
2120
+ state?.resolve(email);
2121
+ setState(null);
2122
+ }, 500);
2123
+ }
2124
+ function handleCancel() {
2125
+ setVisible(false);
2126
+ setTimeout(() => {
2127
+ state?.resolve(null);
2128
+ setState(null);
2129
+ }, 500);
2130
+ }
2131
+ function handleClose() {
2132
+ setVisible(false);
2133
+ setTimeout(() => {
2134
+ state?.resolve(null);
2135
+ setState(null);
2136
+ }, 500);
2137
+ }
2138
+ const value = useMemo7(() => ({ showEmailModal }), [showEmailModal]);
2139
+ return /* @__PURE__ */ jsxs36(EmailModalContext.Provider, { value, children: [
2140
+ children,
2141
+ state && typeof document !== "undefined" && createPortal6(
2142
+ /* @__PURE__ */ jsx41("div", { className: "fixed inset-0 z-40 bg-black/20 backdrop-blur-sm", children: /* @__PURE__ */ jsx41(
2143
+ EmailModalElement,
2144
+ {
2145
+ message,
2146
+ visible,
2147
+ onConfirmAction: handleConfirm,
2148
+ onCancelAction: handleCancel,
2149
+ onCloseModalAction: handleClose
2150
+ }
2151
+ ) }),
2152
+ document.getElementById("email-modal-root")
2153
+ )
2154
+ ] });
2155
+ }
2156
+ function useEmailModal() {
2157
+ const context = useContext7(EmailModalContext);
2158
+ if (!context) {
2159
+ throw new Error("useEmailModal must be used within a EmailModalProvider");
2160
+ }
2161
+ return context;
2162
+ }
2163
+
2164
+ // src/ui/modals/providers.tsx
2165
+ import { Fragment as Fragment6, jsx as jsx42, jsxs as jsxs37 } from "react/jsx-runtime";
2166
+ function Providers({ children }) {
2167
+ return /* @__PURE__ */ jsxs37(Fragment6, { children: [
2168
+ /* @__PURE__ */ jsx42("div", { id: "toast-root" }),
2169
+ /* @__PURE__ */ jsx42("div", { id: "confirm-root" }),
2170
+ /* @__PURE__ */ jsx42("div", { id: "draft-modal-root" }),
2171
+ /* @__PURE__ */ jsx42("div", { id: "import-topics-modal-root" }),
2172
+ /* @__PURE__ */ jsx42("div", { id: "password-modal-root" }),
2173
+ /* @__PURE__ */ jsx42("div", { id: "email-modal-root" }),
2174
+ /* @__PURE__ */ jsx42(ToastProvider, { children: /* @__PURE__ */ jsx42(ConfirmProvider, { children: /* @__PURE__ */ jsx42(DraftModalProvider, { children: /* @__PURE__ */ jsx42(ImportTopicsModalProvider, { children: /* @__PURE__ */ jsx42(EmailModalProvider, { children: /* @__PURE__ */ jsx42(PasswordModalProvider, { children }) }) }) }) }) })
2175
+ ] });
2176
+ }
2177
+
2178
+ // src/ui/nav/desktop/nav-link.tsx
2179
+ import { clsx as clsx13 } from "clsx";
2180
+ import { jsx as jsx43 } from "react/jsx-runtime";
2181
+ function NavLink({ name, url, exact = false }) {
2182
+ const Link = useGoralysUI().Link;
2183
+ const current = useGoralysUI().usePathname();
2184
+ const isActive = exact || url === "/" ? current === url : current === url || current.startsWith(`${url}/`);
2185
+ return /* @__PURE__ */ jsx43(
2186
+ Link,
2187
+ {
2188
+ className: clsx13(
2189
+ "hover:bg-sky-200 h-12.5 w-full items-center flex flex-row hover:text-sky-600 left-1 rounded-md transition-colors p-2",
2190
+ {
2191
+ "bg-sky-200 text-sky-600": isActive,
2192
+ "bg-gray-100 text-gray-900": !isActive
2193
+ }
2194
+ ),
2195
+ href: url,
2196
+ children: name
2197
+ }
2198
+ );
2199
+ }
2200
+
2201
+ // src/ui/nav/desktop/user-nav.tsx
2202
+ import { useEffect as useEffect10, useState as useState24 } from "react";
2203
+ import { cookiesGet as cookiesGet3, emptyUserCacheClient, FULL_NAME_KEY, onUserEvent, USERNAME_KEY } from "@goralys/core";
2204
+ import { UserCircleIcon } from "@heroicons/react/24/outline";
2205
+ import clsx14 from "clsx";
2206
+ import { jsx as jsx44, jsxs as jsxs38 } from "react/jsx-runtime";
2207
+ function UserNav() {
2208
+ const Link = useGoralysUI().Link;
2209
+ const [text, setText] = useState24(null);
2210
+ const [loggedIn, setLoggedIn] = useState24(false);
2211
+ const current = useGoralysUI().usePathname();
2212
+ const targetUrl = loggedIn ? "/user/me" : "/user/login";
2213
+ const isActive = current === targetUrl || current.startsWith(`${targetUrl}/`);
2214
+ useEffect10(() => {
2215
+ const run = () => {
2216
+ const isLoggedIn = !!cookiesGet3(USERNAME_KEY);
2217
+ setLoggedIn(isLoggedIn);
2218
+ let name = isLoggedIn ? cookiesGet3(FULL_NAME_KEY) ?? "" : "Se connecter";
2219
+ if (name.length > 20) name = name.substring(0, 19) + "...";
2220
+ setText(name);
2221
+ };
2222
+ run();
2223
+ }, []);
2224
+ useEffect10(() => {
2225
+ const unsubscribe = onUserEvent((event) => {
2226
+ const isLoggedIn = event === "login";
2227
+ if (!isLoggedIn) {
2228
+ emptyUserCacheClient();
2229
+ }
2230
+ setLoggedIn(isLoggedIn);
2231
+ let name = isLoggedIn ? cookiesGet3(FULL_NAME_KEY) ?? "" : "Se connecter";
2232
+ if (name.length > 17) name = name.substring(0, 15) + "...";
2233
+ setText(name);
2234
+ });
2235
+ return () => {
2236
+ unsubscribe?.();
2237
+ };
2238
+ }, []);
2239
+ return /* @__PURE__ */ jsxs38(
2240
+ Link,
2241
+ {
2242
+ className: clsx14(
2243
+ "h-12.5 w-full flex items-center gap-2 rounded-md transition-colors p-1.5",
2244
+ "hover:bg-sky-200 hover:text-sky-600",
2245
+ {
2246
+ "bg-sky-200 text-sky-600": isActive,
2247
+ "bg-gray-100 text-gray-900": !isActive
2248
+ }
2249
+ ),
2250
+ href: targetUrl,
2251
+ children: [
2252
+ loggedIn && /* @__PURE__ */ jsx44(UserCircleIcon, { width: 27.5, className: "-mr-1.25" }),
2253
+ text
2254
+ ]
2255
+ }
2256
+ );
2257
+ }
2258
+
2259
+ // src/ui/nav/desktop/side-nav.tsx
2260
+ import { useEffect as useEffect11, useState as useState25 } from "react";
2261
+ import { buildArray, cookiesGet as cookiesGet4, cookiesOnChange, ROLE_KEY, USER_ROLES } from "@goralys/core";
2262
+ import { jsx as jsx45, jsxs as jsxs39 } from "react/jsx-runtime";
2263
+ function SideNav() {
2264
+ const Image = useGoralysUI().Image;
2265
+ const [role, setRole] = useState25("none");
2266
+ useEffect11(() => {
2267
+ const run = () => {
2268
+ const current = cookiesGet4(ROLE_KEY) ?? "none";
2269
+ setRole(USER_ROLES.includes(current) ? current : "none");
2270
+ };
2271
+ const onChange = () => {
2272
+ const role2 = cookiesGet4(ROLE_KEY) ?? "none";
2273
+ setRole(USER_ROLES.includes(role2) ? role2 : "none");
2274
+ };
2275
+ run();
2276
+ return cookiesOnChange(onChange);
2277
+ }, []);
2278
+ function getSubjectLinkText() {
2279
+ switch (role) {
2280
+ case "student":
2281
+ return "Mes Questions";
2282
+ case "teacher":
2283
+ return "Mes \xC9l\xE8ves";
2284
+ case "admin":
2285
+ return "Questions";
2286
+ case "none":
2287
+ return "Mon Espace";
2288
+ }
2289
+ }
2290
+ const links = buildArray(
2291
+ { name: "Accueil", url: "/" },
2292
+ { name: getSubjectLinkText(), url: "/subject" },
2293
+ role == "admin" && { name: "Utilisateurs", url: "/admin/users" },
2294
+ role == "admin" && { name: "Acc\xE8s", url: "/admin/admins" },
2295
+ role == "admin" && { name: "Support", url: "/support" }
2296
+ );
2297
+ return /* @__PURE__ */ jsxs39("div", { className: "min-w-50 w-55 h-auto min-h-screen fixed top-0 flex flex-col m-0 p-2 rounded-xl", children: [
2298
+ /* @__PURE__ */ jsx45("div", { className: "flex rounded-md min-w-full h-25 bg-sky-500 mb-2 p-4", children: /* @__PURE__ */ jsx45(
2299
+ Image,
2300
+ {
2301
+ src: "/logo/goralys-logo.svg",
2302
+ width: 125,
2303
+ height: 10,
2304
+ alt: "Goralys logo",
2305
+ loading: "eager",
2306
+ priority: true,
2307
+ className: "-ml-3 self-center w-auto h-auto"
2308
+ }
2309
+ ) }),
2310
+ /* @__PURE__ */ jsx45("div", { className: "flex flex-col gap-2", children: links.map((link) => /* @__PURE__ */ jsx45(NavLink, { name: link.name, url: link.url }, link.url)) }),
2311
+ /* @__PURE__ */ jsx45("div", { className: "grow bg-gray-100 rounded-xl my-3" }),
2312
+ /* @__PURE__ */ jsx45(NavLink, { name: "Centre d'Aide", url: "/help" }, "/help"),
2313
+ /* @__PURE__ */ jsx45(UserNav, {})
2314
+ ] });
2315
+ }
2316
+
2317
+ // src/ui/nav/mobile/tap-bar.tsx
2318
+ import { buildArray as buildArray2 } from "@goralys/core";
2319
+ import { AcademicCapIcon as AcademicCapIcon2, HomeIcon } from "@heroicons/react/24/outline";
2320
+
2321
+ // src/ui/nav/mobile/tap-bar-link.tsx
2322
+ import { clsx as clsx15 } from "clsx";
2323
+ import { jsx as jsx46, jsxs as jsxs40 } from "react/jsx-runtime";
2324
+ function TapBarLink({ name, url, icon: Icon, exact = false }) {
2325
+ const Link = useGoralysUI().Link;
2326
+ const current = useGoralysUI().usePathname();
2327
+ const isActive = exact || url === "/" ? current === url : current === url || current.startsWith(`${url}/`);
2328
+ return /* @__PURE__ */ jsxs40(Link, { className: "flex flex-col w-15 items-center", href: url, children: [
2329
+ /* @__PURE__ */ jsx46(Icon, { className: "w-6 h-6 relative top-1" }),
2330
+ /* @__PURE__ */ jsx46("p", { className: "text-sm", children: name }),
2331
+ /* @__PURE__ */ jsx46(
2332
+ "span",
2333
+ {
2334
+ className: clsx15("relative bottom-0 w-full h-0.5", {
2335
+ "bg-sky-400": isActive,
2336
+ "bg-sky-200": !isActive
2337
+ })
2338
+ }
2339
+ )
2340
+ ] });
2341
+ }
2342
+
2343
+ // src/ui/nav/mobile/tap-bar-user.tsx
2344
+ import { useEffect as useEffect12, useState as useState26 } from "react";
2345
+ import { cookiesGet as cookiesGet5, emptyUserCacheClient as emptyUserCacheClient2, onUserEvent as onUserEvent2, USERNAME_KEY as USERNAME_KEY2 } from "@goralys/core";
2346
+ import { UserCircleIcon as UserCircleIcon2 } from "@heroicons/react/24/outline";
2347
+ import clsx16 from "clsx";
2348
+ import { jsx as jsx47, jsxs as jsxs41 } from "react/jsx-runtime";
2349
+ function TapBarUser() {
2350
+ const Link = useGoralysUI().Link;
2351
+ const [text, setText] = useState26(null);
2352
+ const [loggedIn, setLoggedIn] = useState26(false);
2353
+ const current = useGoralysUI().usePathname();
2354
+ const targetUrl = loggedIn ? "/user/me" : "/user/login";
2355
+ const isActive = current === targetUrl || current.startsWith(`${targetUrl}/`);
2356
+ useEffect12(() => {
2357
+ const run = () => {
2358
+ const isLoggedIn = !!cookiesGet5(USERNAME_KEY2);
2359
+ setLoggedIn(isLoggedIn);
2360
+ setText(isLoggedIn ? "Mon compte" : "Se connecter");
2361
+ };
2362
+ run();
2363
+ }, []);
2364
+ useEffect12(() => {
2365
+ const unsubscribe = onUserEvent2((event) => {
2366
+ const isLoggedIn = event === "login";
2367
+ if (!isLoggedIn) {
2368
+ emptyUserCacheClient2();
2369
+ }
2370
+ setLoggedIn(isLoggedIn);
2371
+ setText(isLoggedIn ? "Mon compte" : "Se connecter");
2372
+ });
2373
+ return () => {
2374
+ unsubscribe?.();
2375
+ };
2376
+ }, []);
2377
+ return /* @__PURE__ */ jsxs41(Link, { className: "flex flex-col w-20 items-center", href: targetUrl, children: [
2378
+ /* @__PURE__ */ jsx47(UserCircleIcon2, { className: "w-6 h-6 relative top-1" }),
2379
+ /* @__PURE__ */ jsx47("p", { className: "text-sm", children: text }),
2380
+ /* @__PURE__ */ jsx47(
2381
+ "span",
2382
+ {
2383
+ className: clsx16("relative bottom-0 w-full h-0.5", {
2384
+ "bg-sky-400": isActive,
2385
+ "bg-sky-200": !isActive
2386
+ })
2387
+ }
2388
+ )
2389
+ ] });
2390
+ }
2391
+
2392
+ // src/ui/nav/mobile/tap-bar.tsx
2393
+ import { jsx as jsx48, jsxs as jsxs42 } from "react/jsx-runtime";
2394
+ function TapBar() {
2395
+ const links = buildArray2(
2396
+ { name: "Accueil", url: "/", icon: HomeIcon },
2397
+ { name: "Sujets", url: "/subject", icon: AcademicCapIcon2 }
2398
+ );
2399
+ return /* @__PURE__ */ jsxs42("div", { className: "z-50 fixed bottom-0 w-full h-12 bg-white flex flex-row content-center justify-evenly", children: [
2400
+ links.map((l) => /* @__PURE__ */ jsx48(TapBarLink, { name: l.name, url: l.url, icon: l.icon }, l.url)),
2401
+ /* @__PURE__ */ jsx48(TapBarUser, {})
2402
+ ] });
2403
+ }
2404
+
2405
+ // src/ui/nav/dynamic-nav.tsx
2406
+ import { jsx as jsx49 } from "react/jsx-runtime";
2407
+ function DynamicNav() {
2408
+ const isDesktop = useMediaQuery("(min-width: 640px)");
2409
+ return isDesktop ? /* @__PURE__ */ jsx49(SideNav, {}) : /* @__PURE__ */ jsx49(TapBar, {});
2410
+ }
2411
+
2412
+ // src/ui/skeletons/button.tsx
2413
+ import { jsx as jsx50 } from "react/jsx-runtime";
2414
+ function ButtonSkeleton({ className }) {
2415
+ return /* @__PURE__ */ jsx50("div", { className: `skeleton w-full h-10 mt-2 mb-2 rounded-xs ${className ?? ""}` });
2416
+ }
2417
+
2418
+ // src/ui/skeletons/admin-panel/admin-card.tsx
2419
+ import { jsx as jsx51, jsxs as jsxs43 } from "react/jsx-runtime";
2420
+ function AdminPanelCardSkeleton() {
2421
+ return /* @__PURE__ */ jsx51(Card, { className: "flex-col w-175! bg-sky-200 gap-1 p-1 mb-1 mt-1", children: /* @__PURE__ */ jsxs43("div", { className: "flex flex-row justify-between items-center", children: [
2422
+ /* @__PURE__ */ jsxs43("div", { className: "flex flex-row", children: [
2423
+ /* @__PURE__ */ jsx51("div", { className: "skeleton h-7 w-7 rounded-xs mr-1.5" }),
2424
+ /* @__PURE__ */ jsx51("div", { className: "skeleton h-5 w-48 rounded-xs self-center" })
2425
+ ] }),
2426
+ /* @__PURE__ */ jsx51("div", { className: "flex flex-row w-50 gap-1", children: /* @__PURE__ */ jsx51(ButtonSkeleton, { className: "w-50!" }) })
2427
+ ] }) });
2428
+ }
2429
+
2430
+ // src/ui/skeletons/admin-panel/audit-action-card.tsx
2431
+ import clsx17 from "clsx";
2432
+ import { jsx as jsx52, jsxs as jsxs44 } from "react/jsx-runtime";
2433
+ var ROWS = ["w-40", "w-56", "w-32", "w-48", "w-36"];
2434
+ function AuditActionCardSkeleton() {
2435
+ return /* @__PURE__ */ jsxs44(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: [
2436
+ /* @__PURE__ */ jsx52(CardTitle, { title: "Actions" }),
2437
+ ROWS.map((width, i) => /* @__PURE__ */ jsxs44("div", { className: "w-full h-5 flex flex-row items-center", children: [
2438
+ /* @__PURE__ */ jsx52("div", { className: "skeleton h-3 w-28 rounded-xs shrink-0 mr-2" }),
2439
+ /* @__PURE__ */ jsx52("div", { className: clsx17("skeleton h-4 rounded-xs", width) })
2440
+ ] }, i))
2441
+ ] });
2442
+ }
2443
+
2444
+ // src/ui/skeletons/admin-panel/user-card.tsx
2445
+ import { jsx as jsx53, jsxs as jsxs45 } from "react/jsx-runtime";
2446
+ function UserCardSkeleton() {
2447
+ return /* @__PURE__ */ jsx53(Card, { className: "flex-col w-200! bg-sky-200 gap-1 p-1 mb-1 mt-1", children: /* @__PURE__ */ jsxs45("div", { className: "flex flex-row justify-between items-center", children: [
2448
+ /* @__PURE__ */ jsxs45("div", { className: "flex flex-row", children: [
2449
+ /* @__PURE__ */ jsx53("div", { className: "skeleton h-7 w-7 rounded-xs mr-1.5" }),
2450
+ /* @__PURE__ */ jsx53("div", { className: "skeleton h-5 w-48 rounded-xs self-center" })
2451
+ ] }),
2452
+ /* @__PURE__ */ jsx53("div", { className: "flex flex-row w-50 gap-1", children: /* @__PURE__ */ jsx53(ButtonSkeleton, { className: "w-50!" }) })
2453
+ ] }) });
2454
+ }
2455
+
2456
+ // src/ui/skeletons/admin-panel/subjects/user-subjects-teacher-card.tsx
2457
+ import { jsx as jsx54, jsxs as jsxs46 } from "react/jsx-runtime";
2458
+ function UserSubjectsTeacherCardSkeleton() {
2459
+ return /* @__PURE__ */ jsxs46(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: [
2460
+ /* @__PURE__ */ jsx54(CardTitle, { title: "Sujets" }),
2461
+ /* @__PURE__ */ jsxs46(Card, { className: "flex flex-row w-full", children: [
2462
+ /* @__PURE__ */ jsx54("div", { className: "skeleton h-48 w-48 rounded-full shrink-0 mr-2 relative", children: /* @__PURE__ */ jsx54("div", { className: "absolute inset-[15%] rounded-full bg-sky-200" }) }),
2463
+ /* @__PURE__ */ jsx54("span", { className: "basis-1/6 grow-0 shrink min-w-0" }),
2464
+ /* @__PURE__ */ jsx54("ul", { className: "flex flex-col gap-1 self-center mr-10", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsxs46("li", { className: "flex items-center gap-2", children: [
2465
+ /* @__PURE__ */ jsx54("div", { className: "skeleton h-2.5 w-2.5 rounded-full shrink-0" }),
2466
+ /* @__PURE__ */ jsx54("div", { className: "skeleton h-4 w-16 rounded-xs" }),
2467
+ /* @__PURE__ */ jsx54("div", { className: "skeleton h-4 w-10 rounded-xs ml-auto" })
2468
+ ] }, i)) })
2469
+ ] })
2470
+ ] });
2471
+ }
2472
+
2473
+ // src/ui/skeletons/admin-panel/subjects/user-subjetcs-student-card.tsx
2474
+ import { jsx as jsx55, jsxs as jsxs47 } from "react/jsx-runtime";
2475
+ function UserSubjectsStudentCardSkeleton() {
2476
+ return /* @__PURE__ */ jsx55(Card, { className: "flex flex-col grow bg-sky-200 m-2", children: Array.from({ length: 2 }).map((_, i) => /* @__PURE__ */ jsxs47(Card, { className: "flex flex-row w-full justify-between", children: [
2477
+ /* @__PURE__ */ jsxs47("div", { className: "flex flex-col gap-1.5", children: [
2478
+ /* @__PURE__ */ jsx55("div", { className: "skeleton h-5 w-16 rounded-xs" }),
2479
+ /* @__PURE__ */ jsx55("div", { className: "skeleton h-4 w-24 rounded-xs" })
2480
+ ] }),
2481
+ /* @__PURE__ */ jsx55("div", { className: "skeleton h-12 w-12 rounded-full shrink-0 relative", children: /* @__PURE__ */ jsx55("div", { className: "absolute inset-[15%] rounded-full bg-sky-200" }) })
2482
+ ] }, i)) });
2483
+ }
2484
+
2485
+ // src/ui/skeletons/inputs/subject-input.tsx
2486
+ import { jsx as jsx56, jsxs as jsxs48 } from "react/jsx-runtime";
2487
+ function SubjectInputSkeleton({ showMeta = true }) {
2488
+ return /* @__PURE__ */ jsxs48("div", { className: "relative mt-3 min-w-50 mb-5", children: [
2489
+ /* @__PURE__ */ jsx56("div", { className: "skeleton w-full h-10 rounded-xs" }),
2490
+ showMeta && /* @__PURE__ */ jsxs48("div", { className: "flex flex-row justify-between mt-1.5", children: [
2491
+ /* @__PURE__ */ jsx56("div", { className: "skeleton h-3 w-24 rounded-xs" }),
2492
+ /* @__PURE__ */ jsx56("div", { className: "skeleton h-4 w-32 rounded-xs" })
2493
+ ] })
2494
+ ] });
2495
+ }
2496
+
2497
+ // src/ui/skeletons/subjects/admin-card.tsx
2498
+ import { jsx as jsx57, jsxs as jsxs49 } from "react/jsx-runtime";
2499
+ function AdminSubjectCardSkeleton() {
2500
+ return /* @__PURE__ */ jsxs49("div", { className: "h-fit w-200 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
2501
+ /* @__PURE__ */ jsxs49("div", { className: "flex flex-row w-full justify-between", children: [
2502
+ /* @__PURE__ */ jsx57("div", { className: "skeleton h-5 w-24 rounded-xs" }),
2503
+ /* @__PURE__ */ jsx57("div", { className: "skeleton h-5 w-40 rounded-xs" })
2504
+ ] }),
2505
+ /* @__PURE__ */ jsx57(SubjectInputSkeleton, { showMeta: false })
2506
+ ] });
2507
+ }
2508
+
2509
+ // src/ui/skeletons/subjects/comment.tsx
2510
+ import { jsx as jsx58, jsxs as jsxs50 } from "react/jsx-runtime";
2511
+ function CommentSkeleton() {
2512
+ return /* @__PURE__ */ jsxs50("div", { className: "flex flex-row items-center gap-1.5", children: [
2513
+ /* @__PURE__ */ jsx58("div", { className: "skeleton h-4 w-4 rounded-xs" }),
2514
+ /* @__PURE__ */ jsx58("div", { className: "skeleton h-4 w-44 rounded-xs" })
2515
+ ] });
2516
+ }
2517
+
2518
+ // src/ui/skeletons/subjects/student-card.tsx
2519
+ import { Fragment as Fragment7, jsx as jsx59, jsxs as jsxs51 } from "react/jsx-runtime";
2520
+ function StudentCardSkeleton({ showButtons = true }) {
2521
+ return /* @__PURE__ */ jsxs51("div", { className: "h-fit sm:w-200 w-96 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
2522
+ /* @__PURE__ */ jsxs51("div", { className: "flex flex-row w-full justify-between", children: [
2523
+ /* @__PURE__ */ jsx59("div", { className: "skeleton h-5 w-24 rounded-xs" }),
2524
+ /* @__PURE__ */ jsx59("div", { className: "skeleton h-5 w-24 rounded-xs" })
2525
+ ] }),
2526
+ /* @__PURE__ */ jsx59(SubjectInputSkeleton, {}),
2527
+ /* @__PURE__ */ jsx59(CommentSkeleton, {}),
2528
+ showButtons && /* @__PURE__ */ jsxs51(Fragment7, { children: [
2529
+ /* @__PURE__ */ jsx59(ButtonSkeleton, { className: "mb-1! mt-1!" }),
2530
+ /* @__PURE__ */ jsx59(ButtonSkeleton, { className: "mb-1! mt-1!" })
2531
+ ] })
2532
+ ] });
2533
+ }
2534
+
2535
+ // src/ui/skeletons/subjects/teacher-card.tsx
2536
+ import { Fragment as Fragment8, jsx as jsx60, jsxs as jsxs52 } from "react/jsx-runtime";
2537
+ function TeacherCardSkeleton({ showButtons = false }) {
2538
+ return /* @__PURE__ */ jsxs52("div", { className: "h-fit sm:w-200 w-96 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
2539
+ /* @__PURE__ */ jsxs52("div", { className: "flex flex-row w-full justify-between", children: [
2540
+ /* @__PURE__ */ jsx60("div", { className: "skeleton h-5 w-24 rounded-xs" }),
2541
+ /* @__PURE__ */ jsx60("div", { className: "skeleton h-5 w-24 rounded-xs" })
2542
+ ] }),
2543
+ /* @__PURE__ */ jsx60(SubjectInputSkeleton, {}),
2544
+ /* @__PURE__ */ jsx60(CommentSkeleton, {}),
2545
+ showButtons && /* @__PURE__ */ jsxs52(Fragment8, { children: [
2546
+ /* @__PURE__ */ jsx60(ButtonSkeleton, { className: "-mb-1! mt-1!" }),
2547
+ /* @__PURE__ */ jsx60(ButtonSkeleton, { className: "mb-1! mt-1!" })
2548
+ ] })
2549
+ ] });
2550
+ }
2551
+
2552
+ // src/ui/skeletons/support/support-ticket-card.tsx
2553
+ import { jsx as jsx61, jsxs as jsxs53 } from "react/jsx-runtime";
2554
+ function SupportTicketCardSkeleton() {
2555
+ return /* @__PURE__ */ jsxs53("div", { className: "h-fit w-200 flex flex-col bg-sky-200 gap-1 gap-y-3 p-1 mt-1", children: [
2556
+ /* @__PURE__ */ jsxs53("div", { className: "flex flex-row w-full justify-between", children: [
2557
+ /* @__PURE__ */ jsx61("div", { className: "skeleton h-5 w-32 rounded-xs" }),
2558
+ /* @__PURE__ */ jsx61("div", { className: "skeleton h-5 w-24 rounded-xs" })
2559
+ ] }),
2560
+ /* @__PURE__ */ jsx61("div", { className: "skeleton h-24 w-full rounded-xs" }),
2561
+ /* @__PURE__ */ jsx61("div", { className: "skeleton h-10 w-full rounded-xs" })
2562
+ ] });
2563
+ }
2564
+
2565
+ // src/ui/subjects/admin-card.tsx
2566
+ import {
2567
+ cookiesSet as cookiesSet3,
2568
+ fetchCsrfClient as fetchCsrfClient3,
2569
+ getStatusLabel as getStatusLabel2,
2570
+ goralysFetchClient as goralysFetchClient3,
2571
+ handleToastRequest as handleToastRequest3,
2572
+ SubjectStatusConfig
2573
+ } from "@goralys/core";
2574
+ import { jsx as jsx62, jsxs as jsxs54 } from "react/jsx-runtime";
2575
+ function AdminSubjectsCard({ subjectData, onUpdateAction, syncKey }) {
2576
+ const password = usePasswordModal();
2577
+ const toast = useToast();
2578
+ const updateStatus = async (status) => {
2579
+ if (status === subjectData.status) return;
2580
+ const pwd = await password.showPasswordModal("le changement de statut");
2581
+ if (!pwd) return;
2582
+ if (pwd.trim() === "") {
2583
+ toast.showToast({
2584
+ type: "warning",
2585
+ title: "Mot de passe",
2586
+ message: "Veuillez saisir un mot de passe."
2587
+ });
2588
+ return;
2589
+ }
2590
+ const csrfToken = await fetchCsrfClient3("update-subject-status");
2591
+ const payload = {
2592
+ student: subjectData.studentToken,
2593
+ teacher: subjectData.teacherToken,
2594
+ status,
2595
+ topic: subjectData.topicCode,
2596
+ "admin-password": pwd,
2597
+ "csrf-token": csrfToken
2598
+ };
2599
+ const res = await goralysFetchClient3("PATCH", "subjects/status", payload);
2600
+ await handleToastRequest3(res, toast.showToast, false);
2601
+ const data = await res?.json();
2602
+ if (data.toastType === "info" && res.ok) {
2603
+ cookiesSet3(syncKey, "0");
2604
+ onUpdateAction();
2605
+ }
2606
+ };
2607
+ return /* @__PURE__ */ jsxs54("div", { className: "h-fit w-200 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
2608
+ /* @__PURE__ */ jsxs54("div", { className: "flex flex-row w-full justify-between", children: [
2609
+ /* @__PURE__ */ jsx62("strong", { children: subjectData.student }),
2610
+ /* @__PURE__ */ jsxs54("strong", { children: [
2611
+ subjectData.topic,
2612
+ " (",
2613
+ subjectData.teacher,
2614
+ ")"
2615
+ ] })
2616
+ ] }),
2617
+ /* @__PURE__ */ jsx62(
2618
+ SubjectInputAdmin,
2619
+ {
2620
+ id: subjectData.studentToken + subjectData.teacherToken + "-input",
2621
+ subjectData,
2622
+ label: "Question de l'El\xE8ve"
2623
+ }
2624
+ ),
2625
+ /* @__PURE__ */ jsxs54("div", { className: "flex flex-row justify-between", children: [
2626
+ /* @__PURE__ */ jsxs54("p", { children: [
2627
+ "Statut de la question: ",
2628
+ getStatusLabel2(subjectData.status)
2629
+ ] }),
2630
+ /* @__PURE__ */ jsxs54("div", { className: "relative pb-0 mb-1", children: [
2631
+ /* @__PURE__ */ jsx62(
2632
+ "select",
2633
+ {
2634
+ className: "border-0 border-b-2 border-sky-300 appearance-none\n cursor-pointer outline-none focus:ring-0 text-base leading-5\n text-heading pb-0 pr-5 subjects-search-select",
2635
+ value: subjectData.status,
2636
+ onChange: (e) => updateStatus(e.target.value),
2637
+ children: Object.entries(SubjectStatusConfig).map(([key, info]) => /* @__PURE__ */ jsx62("option", { value: key, children: info.label }, key))
2638
+ }
2639
+ ),
2640
+ /* @__PURE__ */ jsx62(
2641
+ "span",
2642
+ {
2643
+ className: "absolute bottom-0 left-0 w-0 h-0.5 bg-sky-500\n transition-all duration-300 ease-in-out\n peer-focus:w-full subjects-search-underline"
2644
+ }
2645
+ )
2646
+ ] })
2647
+ ] })
2648
+ ] });
2649
+ }
2650
+
2651
+ // src/ui/subjects/comment-student.tsx
2652
+ import { ChevronRightIcon } from "@heroicons/react/24/outline";
2653
+ import { Fragment as Fragment9, jsx as jsx63, jsxs as jsxs55 } from "react/jsx-runtime";
2654
+ function CommentStudent({ subjectData, disabled, onChange }) {
2655
+ const visible = !!subjectData?.comment && !(subjectData.status === "submitted" || subjectData.status === "approved");
2656
+ const showLastRejected = subjectData.status === "not_submitted" && subjectData.lastRejected;
2657
+ if (!visible) {
2658
+ return /* @__PURE__ */ jsx63(Fragment9, {});
2659
+ }
2660
+ return /* @__PURE__ */ jsx63(Fragment9, { children: /* @__PURE__ */ jsxs55(
2661
+ "details",
2662
+ {
2663
+ className: "group",
2664
+ open: subjectData.status === "rejected",
2665
+ children: [
2666
+ /* @__PURE__ */ jsxs55("summary", { className: "flex flex-row cursor-pointer", children: [
2667
+ /* @__PURE__ */ jsx63(ChevronRightIcon, { className: "w-5 h-5 transition-transform duration-200 group-open:rotate-90" }),
2668
+ /* @__PURE__ */ jsx63("span", { children: "Commentaire du professeur" })
2669
+ ] }),
2670
+ showLastRejected && /* @__PURE__ */ jsxs55(Fragment9, { children: [
2671
+ /* @__PURE__ */ jsx63("span", { className: "h-2 w-full block" }),
2672
+ /* @__PURE__ */ jsx63(
2673
+ SubjectTextArea,
2674
+ {
2675
+ id: subjectData.studentToken + subjectData.teacherToken + "-last-rejected",
2676
+ label: "Votre question non valid\xE9e",
2677
+ defaultValue: subjectData.lastRejected,
2678
+ disabled: true,
2679
+ animate: false,
2680
+ subjectData
2681
+ }
2682
+ )
2683
+ ] }),
2684
+ /* @__PURE__ */ jsx63("span", { className: "h-1.5 w-full block" }),
2685
+ /* @__PURE__ */ jsx63(
2686
+ TextArea,
2687
+ {
2688
+ id: subjectData.studentToken + subjectData.teacherToken + "-subject-comment",
2689
+ label: "Commentaire",
2690
+ defaultValue: subjectData.comment,
2691
+ onChangeAction: onChange,
2692
+ disabled
2693
+ }
2694
+ )
2695
+ ]
2696
+ },
2697
+ `comment-student-details-for-${subjectData.teacher}-${subjectData.topic}`
2698
+ ) });
2699
+ }
2700
+
2701
+ // src/ui/subjects/comment-teacher.tsx
2702
+ import { ChevronRightIcon as ChevronRightIcon2 } from "@heroicons/react/24/outline";
2703
+ import { Fragment as Fragment10, jsx as jsx64, jsxs as jsxs56 } from "react/jsx-runtime";
2704
+ function CommentTeacher({ subjectData, disabled, ref, onChange }) {
2705
+ const visible = subjectData.status === "submitted";
2706
+ if (!visible) {
2707
+ return /* @__PURE__ */ jsx64(Fragment10, {});
2708
+ }
2709
+ return /* @__PURE__ */ jsx64(Fragment10, { children: /* @__PURE__ */ jsxs56(
2710
+ "details",
2711
+ {
2712
+ className: "group",
2713
+ open: subjectData.status === "rejected",
2714
+ children: [
2715
+ /* @__PURE__ */ jsxs56("summary", { className: "flex flex-row cursor-pointer", children: [
2716
+ /* @__PURE__ */ jsx64(ChevronRightIcon2, { className: "w-5 h-5 transition-transform duration-200 group-open:rotate-90" }),
2717
+ /* @__PURE__ */ jsx64("span", { children: "Votre commentaire" })
2718
+ ] }),
2719
+ /* @__PURE__ */ jsx64(
2720
+ TextArea,
2721
+ {
2722
+ id: subjectData.studentToken + subjectData.teacherToken + "-subject-comment",
2723
+ label: "Commentaire",
2724
+ defaultValue: subjectData.comment,
2725
+ onChangeAction: onChange,
2726
+ disabled,
2727
+ ref
2728
+ },
2729
+ `comment-teacher-details-for-${subjectData.student}-${subjectData.topic}`
2730
+ )
2731
+ ]
2732
+ },
2733
+ `comment-teacher-details-for-${subjectData.student}-${subjectData.topic}`
2734
+ ) });
2735
+ }
2736
+
2737
+ // src/ui/subjects/readonly-card.tsx
2738
+ import { getStatusLabel as getStatusLabel3 } from "@goralys/core";
2739
+ import { jsx as jsx65, jsxs as jsxs57 } from "react/jsx-runtime";
2740
+ function ReadonlyCard({ subjectData }) {
2741
+ return /* @__PURE__ */ jsxs57("div", { className: "h-fit w-200 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
2742
+ /* @__PURE__ */ jsxs57("div", { className: "flex flex-row w-full justify-between", children: [
2743
+ /* @__PURE__ */ jsx65("strong", { children: subjectData.student }),
2744
+ /* @__PURE__ */ jsxs57("strong", { children: [
2745
+ subjectData.topic,
2746
+ " (",
2747
+ subjectData.teacher,
2748
+ ")"
2749
+ ] })
2750
+ ] }),
2751
+ /* @__PURE__ */ jsx65(
2752
+ SubjectInputAdmin,
2753
+ {
2754
+ id: subjectData.studentToken + subjectData.teacherToken + "-input",
2755
+ subjectData,
2756
+ label: "Question de l'El\xE8ve"
2757
+ }
2758
+ ),
2759
+ /* @__PURE__ */ jsx65("div", { className: "flex flex-row justify-between", children: /* @__PURE__ */ jsxs57("p", { children: [
2760
+ "Statut de la question: ",
2761
+ getStatusLabel3(subjectData.status)
2762
+ ] }) })
2763
+ ] });
2764
+ }
2765
+
2766
+ // src/ui/subjects/student-card.tsx
2767
+ import {
2768
+ cookiesSet as cookiesSet4,
2769
+ fetchCsrfClient as fetchCsrfClient4,
2770
+ getShortFromLong as getShortFromLong2,
2771
+ goralysFetchClient as goralysFetchClient4,
2772
+ handleToastRequest as handleToastRequest4,
2773
+ SUBJECT_SYNCS
2774
+ } from "@goralys/core";
2775
+ import { useState as useState27 } from "react";
2776
+ import { Fragment as Fragment11, jsx as jsx66, jsxs as jsxs58 } from "react/jsx-runtime";
2777
+ function StudentCard({ subjectData, onUpdateAction }) {
2778
+ const toast = useToast();
2779
+ const confirm = useConfirm();
2780
+ const isDesktop = useMediaQuery("(min-width:640px)");
2781
+ const [subject, setSubject] = useState27(subjectData.subject);
2782
+ const [isInterdisciplinary, setIsInterdisciplinary] = useState27(subjectData.interdisciplinary);
2783
+ const modal = useDraftModal();
2784
+ const saveDraft = async () => {
2785
+ if (!subject || subject.trim() == "") {
2786
+ toast.showToast({
2787
+ type: "warning",
2788
+ title: "Brouillon",
2789
+ message: "Veuillez saisir une question."
2790
+ });
2791
+ return;
2792
+ }
2793
+ const csrfToken = await fetchCsrfClient4("save-draft");
2794
+ const payload = {
2795
+ teacher: subjectData.teacherToken,
2796
+ student: subjectData.studentToken,
2797
+ topic: subjectData.topicCode,
2798
+ draft: subject,
2799
+ interdisciplinary: isInterdisciplinary,
2800
+ "csrf-token": csrfToken
2801
+ };
2802
+ const res = await goralysFetchClient4("PUT", "subjects/draft", payload);
2803
+ await handleToastRequest4(res, toast.showToast, false);
2804
+ const data = await res.json();
2805
+ if (data.toastType === "info" && res.ok) {
2806
+ cookiesSet4(SUBJECT_SYNCS["student"], "0");
2807
+ onUpdateAction();
2808
+ }
2809
+ };
2810
+ const sendSubject = async () => {
2811
+ if (!subject || subject.trim() == "") {
2812
+ toast.showToast({
2813
+ type: "warning",
2814
+ title: "Envoi",
2815
+ message: "Veuillez saisir une question."
2816
+ });
2817
+ return;
2818
+ }
2819
+ if (!await confirm.showConfirm({
2820
+ title: "Envoi",
2821
+ message: "\xCAtes-vous s\xFBr de vouloir envoyer cette question au professeur ?"
2822
+ }))
2823
+ return;
2824
+ if (subject?.trim() === subjectData.lastRejected?.trim()) {
2825
+ toast.showToast({
2826
+ type: "warning",
2827
+ title: "Envoi",
2828
+ message: "Cette question n\u2019a pas \xE9t\xE9 modifi\xE9e depuis son invalidation. Merci de la corriger avant de la renvoyer."
2829
+ });
2830
+ return;
2831
+ }
2832
+ const formData = new FormData();
2833
+ if (isDesktop) {
2834
+ const result = await modal.showDraftModal();
2835
+ if (result.type === "closed") return;
2836
+ if (result.type == "withDraft" && !result.file) {
2837
+ toast.showToast({
2838
+ type: "warning",
2839
+ title: "Envoi",
2840
+ message: "Veuillez choisir un brouillon ou envoyer la question seule."
2841
+ });
2842
+ return;
2843
+ }
2844
+ if (result.type == "withDraft") {
2845
+ formData.append("draft-file", result.file ?? "");
2846
+ }
2847
+ }
2848
+ const csrfToken = await fetchCsrfClient4("submit-subject");
2849
+ formData.append("teacher", subjectData.teacherToken);
2850
+ formData.append("student", subjectData.studentToken);
2851
+ formData.append("topic", subjectData.topic);
2852
+ formData.append("subject", subject ?? "");
2853
+ formData.append("csrf-token", csrfToken ?? "");
2854
+ formData.append("interdisciplinary", isInterdisciplinary ? "1" : "0");
2855
+ const res = await goralysFetchClient4("POST", "subjects/submit", formData);
2856
+ if (await handleToastRequest4(res, toast.showToast, false)) {
2857
+ const data = await res.json();
2858
+ if (data.toastType === "info" && res.ok) {
2859
+ cookiesSet4(SUBJECT_SYNCS["student"], "0");
2860
+ onUpdateAction();
2861
+ }
2862
+ }
2863
+ };
2864
+ const key = subjectData.teacher + subjectData.topic;
2865
+ return /* @__PURE__ */ jsxs58("div", { className: "h-fit sm:w-200 w-96 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
2866
+ /* @__PURE__ */ jsxs58("div", { className: "flex flex-row w-full justify-between", children: [
2867
+ /* @__PURE__ */ jsx66("strong", { children: isDesktop ? subjectData.topic : getShortFromLong2(subjectData.topic) }),
2868
+ isDesktop && /* @__PURE__ */ jsx66("strong", { children: subjectData.teacher })
2869
+ ] }),
2870
+ /* @__PURE__ */ jsx66(
2871
+ SubjectInputStudent,
2872
+ {
2873
+ id: `subject-input-student-for-${key}`,
2874
+ label: "Votre Question",
2875
+ subjectData,
2876
+ onChangeAction: (e) => {
2877
+ setSubject(e.target.value);
2878
+ },
2879
+ setIsInterdisciplinaryAction: setIsInterdisciplinary
2880
+ }
2881
+ ),
2882
+ /* @__PURE__ */ jsx66(CommentStudent, { subjectData, disabled: true }, `comment-student-for-${key}`),
2883
+ !(subjectData.status === "submitted" || subjectData.status === "approved") && /* @__PURE__ */ jsxs58(Fragment11, { children: [
2884
+ /* @__PURE__ */ jsx66(Button, { className: "mb-1! mt-1!", text: "Enregistrer comme brouillon", type: "button", onClick: saveDraft }),
2885
+ /* @__PURE__ */ jsx66(Button, { className: "mb-1! mt-1!", text: "Envoyer la question", type: "button", onClick: sendSubject })
2886
+ ] })
2887
+ ] });
2888
+ }
2889
+
2890
+ // src/ui/subjects/subjects-filter.tsx
2891
+ import { useCallback as useCallback8, useEffect as useEffect13, useState as useState28 } from "react";
2892
+ import { getStatusLabel as getStatusLabel4 } from "@goralys/core";
2893
+ import { jsx as jsx67 } from "react/jsx-runtime";
2894
+ function SubjectsFilter({ subjects, setCurrentSubjects, defaults }) {
2895
+ const DEFAULT_STATUS = {
2896
+ not_submitted: { default: defaults?.not_submitted ?? false, label: getStatusLabel4("not_submitted") },
2897
+ submitted: { default: defaults?.submitted ?? true, label: getStatusLabel4("submitted") },
2898
+ rejected: { default: defaults?.rejected ?? false, label: getStatusLabel4("rejected") },
2899
+ approved: { default: defaults?.approved ?? false, label: getStatusLabel4("approved") }
2900
+ };
2901
+ const [shownStatus, setShownStatus] = useState28(
2902
+ Object.keys(DEFAULT_STATUS).filter((k) => DEFAULT_STATUS[k].default)
2903
+ );
2904
+ const toggleStatus = (s) => {
2905
+ console.log("toggling status: ", s);
2906
+ const status = shownStatus;
2907
+ if (shownStatus.includes(s)) {
2908
+ const i = shownStatus.indexOf(s);
2909
+ status.splice(i, 1);
2910
+ } else {
2911
+ status.push(s);
2912
+ }
2913
+ setShownStatus(status);
2914
+ console.log("toggle done, new shown status: ", shownStatus);
2915
+ console.log("filtering");
2916
+ filter();
2917
+ console.log("filtering done");
2918
+ };
2919
+ const filter = useCallback8(() => {
2920
+ const filtered = [];
2921
+ subjects.forEach((s) => {
2922
+ if (shownStatus.includes(s.status)) filtered.push(s);
2923
+ });
2924
+ console.log(filtered);
2925
+ setCurrentSubjects(filtered);
2926
+ }, [shownStatus, subjects, setCurrentSubjects]);
2927
+ useEffect13(() => {
2928
+ filter();
2929
+ }, [filter]);
2930
+ return /* @__PURE__ */ jsx67("div", { className: "flex flex-row gap-2 w-full", children: Object.entries(DEFAULT_STATUS).map(([k, v]) => /* @__PURE__ */ jsx67(
2931
+ Checkbox,
2932
+ {
2933
+ label: v.label,
2934
+ setValueAction: () => toggleStatus(k),
2935
+ defaultValue: DEFAULT_STATUS[k].default
2936
+ },
2937
+ "subjects-filter-checkbox-for-" + k
2938
+ )) });
2939
+ }
2940
+
2941
+ // src/ui/subjects/subjects-search-bar.tsx
2942
+ import { useEffect as useEffect14, useState as useState29 } from "react";
2943
+ import { getLongFromShort, searchFields } from "@goralys/core";
2944
+ import { jsx as jsx68, jsxs as jsxs59 } from "react/jsx-runtime";
2945
+ function SubjectsSearchBar({ subjects, setCurrentSubjects }) {
2946
+ const [currentField, setCurrentField] = useState29("all");
2947
+ const [searchText, setSearchText] = useState29("");
2948
+ const handleSearch = (e) => {
2949
+ const value = e.currentTarget.value;
2950
+ setSearchText(value);
2951
+ };
2952
+ const sortSubjects = (list) => {
2953
+ return [...list].sort((a, b) => {
2954
+ const topicA = a.topic.trim().toLowerCase();
2955
+ const topicB = b.topic.trim().toLowerCase();
2956
+ const nameA = a.student.trim().toLowerCase();
2957
+ const nameB = b.student.trim().toLowerCase();
2958
+ const topicDiff = topicA.localeCompare(topicB, "fr");
2959
+ if (topicDiff !== 0) return topicDiff;
2960
+ return nameA.localeCompare(nameB, "fr");
2961
+ });
2962
+ };
2963
+ useEffect14(() => {
2964
+ if (!subjects) return;
2965
+ const search = searchText.trim().toLowerCase();
2966
+ const sorted = sortSubjects(
2967
+ subjects.filter((s) => {
2968
+ if (!search) return true;
2969
+ const searchTeachers = s.teacher.split(",").map((t) => t.trim().toLowerCase());
2970
+ switch (currentField) {
2971
+ case "student":
2972
+ return s.student.trim().toLowerCase().startsWith(search);
2973
+ case "teacher":
2974
+ return searchTeachers.some((t) => t.startsWith(search));
2975
+ case "topic":
2976
+ return s.topic.trim().toLowerCase().includes(getLongFromShort(search));
2977
+ case "all":
2978
+ return s.student.trim().toLowerCase().startsWith(search) || searchTeachers.some((t) => t.startsWith(search)) || s.topic.trim().toLowerCase().includes(getLongFromShort(search));
2979
+ default:
2980
+ return true;
2981
+ }
2982
+ })
2983
+ );
2984
+ setCurrentSubjects((prev) => {
2985
+ if (JSON.stringify(prev) === JSON.stringify(sorted)) return prev;
2986
+ return sorted;
2987
+ });
2988
+ }, [searchText, currentField, subjects, setCurrentSubjects]);
2989
+ return /* @__PURE__ */ jsxs59("div", { className: "flex flex-row gap-2 items-end mb-4", children: [
2990
+ /* @__PURE__ */ jsx68(FloatingInput, { id: "admins-subjects-search", label: "Rechercher", onInput: handleSearch }),
2991
+ /* @__PURE__ */ jsxs59("div", { className: "relative pb-0 mb-1", children: [
2992
+ /* @__PURE__ */ jsx68(
2993
+ "select",
2994
+ {
2995
+ className: "border-0 border-b-2 border-sky-300 appearance-none\n cursor-pointer outline-none focus:ring-0 text-base leading-5\n text-heading pb-0 pr-5 subjects-search-select",
2996
+ value: currentField,
2997
+ onChange: (e) => setCurrentField(e.target.value),
2998
+ children: Object.entries(searchFields).map(([key, label]) => /* @__PURE__ */ jsx68("option", { value: key, children: label }, key))
2999
+ }
3000
+ ),
3001
+ /* @__PURE__ */ jsx68(
3002
+ "span",
3003
+ {
3004
+ className: "absolute bottom-0 left-0 w-0 h-0.5 bg-sky-500\n transition-all duration-300 ease-in-out\n peer-focus:w-full subjects-search-underline"
3005
+ }
3006
+ )
3007
+ ] })
3008
+ ] });
3009
+ }
3010
+
3011
+ // src/ui/subjects/teacher-card.tsx
3012
+ import {
3013
+ cookiesSet as cookiesSet5,
3014
+ fetchCsrfClient as fetchCsrfClient5,
3015
+ getShortFromLong as getShortFromLong3,
3016
+ goralysFetchClient as goralysFetchClient5,
3017
+ handleToastRequest as handleToastRequest5,
3018
+ SUBJECT_SYNCS as SUBJECT_SYNCS2
3019
+ } from "@goralys/core";
3020
+ import { useRef as useRef7, useState as useState30 } from "react";
3021
+ import { Fragment as Fragment12, jsx as jsx69, jsxs as jsxs60 } from "react/jsx-runtime";
3022
+ function TeacherCard({ subjectData, onUpdateAction }) {
3023
+ const toast = useToast();
3024
+ const confirm = useConfirm();
3025
+ const [comment, setComment] = useState30(subjectData.comment);
3026
+ const commentRef = useRef7(null);
3027
+ const isDesktop = useMediaQuery("(min-width: 640px)");
3028
+ const rejectSubject = async () => {
3029
+ if (comment?.trim() === "" || !comment) {
3030
+ toast.showToast({
3031
+ type: "warning",
3032
+ title: "Commentaire requis",
3033
+ message: "Vous devez fournir un commentaire avant de rejeter cette question."
3034
+ });
3035
+ return;
3036
+ }
3037
+ if (!await confirm.showConfirm({
3038
+ title: "Confirmer le rejet",
3039
+ message: "Une fois la question rejet\xE9e, l'\xE9l\xE8ve devra en soumettre une nouvelle. Voulez-vous quand m\xEAme rejeter cette question ?"
3040
+ }))
3041
+ return;
3042
+ if (comment?.trim() === subjectData.comment.trim()) {
3043
+ const confirmed = await confirm.showConfirm({
3044
+ title: "Confirmer le rejet",
3045
+ message: "Le commentaire n\u2019a pas \xE9t\xE9 modifi\xE9. Voulez-vous quand m\xEAme rejeter cette question ?"
3046
+ });
3047
+ if (!confirmed) {
3048
+ requestAnimationFrame(() => {
3049
+ commentRef?.current?.blur();
3050
+ commentRef?.current?.focus();
3051
+ });
3052
+ return;
3053
+ }
3054
+ }
3055
+ const csrfToken = await fetchCsrfClient5("reject-subject");
3056
+ const payload = {
3057
+ teacher: subjectData.teacherToken,
3058
+ student: subjectData.studentToken,
3059
+ topic: subjectData.topicCode,
3060
+ comment,
3061
+ "csrf-token": csrfToken
3062
+ };
3063
+ const res = await goralysFetchClient5("POST", "subjects/reject", payload);
3064
+ if (await handleToastRequest5(res, toast.showToast, false)) {
3065
+ const data = await res.json();
3066
+ if (data.toastType === "info" && res.ok) {
3067
+ cookiesSet5(SUBJECT_SYNCS2["teacher"], "0");
3068
+ onUpdateAction();
3069
+ }
3070
+ }
3071
+ };
3072
+ const approveSubject = async () => {
3073
+ if (!await confirm.showConfirm({
3074
+ title: "Validation de la question",
3075
+ message: "\xCAtes-vous s\xFBr de vouloir valider cette question de mani\xE8re d\xE9finitive. Une fois la question valid\xE9e, toute modification devient impossible."
3076
+ }))
3077
+ return;
3078
+ const csrfToken = await fetchCsrfClient5("approve-subject");
3079
+ const payload = {
3080
+ teacher: subjectData.teacherToken,
3081
+ student: subjectData.studentToken,
3082
+ topic: subjectData.topicCode,
3083
+ "new-status": "approved",
3084
+ "csrf-token": csrfToken
3085
+ };
3086
+ const res = await goralysFetchClient5("POST", "subjects/approve", payload);
3087
+ if (await handleToastRequest5(res, toast.showToast, false)) {
3088
+ const data = await res.json();
3089
+ if (data.toastType === "info" && res.ok) {
3090
+ cookiesSet5(SUBJECT_SYNCS2["teacher"], "0");
3091
+ onUpdateAction();
3092
+ }
3093
+ }
3094
+ };
3095
+ return /* @__PURE__ */ jsxs60("div", { className: "h-fit sm:w-200 w-96 flex flex-col bg-sky-200 gap-1 p-1 mb-1 mt-1", children: [
3096
+ /* @__PURE__ */ jsxs60("div", { className: "flex flex-row w-full justify-between", children: [
3097
+ /* @__PURE__ */ jsx69("strong", { children: subjectData.student }),
3098
+ /* @__PURE__ */ jsx69("strong", { children: isDesktop ? subjectData.topic : getShortFromLong3(subjectData.topic) })
3099
+ ] }),
3100
+ /* @__PURE__ */ jsx69(
3101
+ SubjectInputTeacher,
3102
+ {
3103
+ id: subjectData.studentToken + subjectData.teacherToken + "-comment",
3104
+ subjectData,
3105
+ label: "Question de l'El\xE8ve"
3106
+ }
3107
+ ),
3108
+ /* @__PURE__ */ jsx69(
3109
+ CommentTeacher,
3110
+ {
3111
+ subjectData,
3112
+ disabled: subjectData.status !== "submitted",
3113
+ ref: commentRef,
3114
+ onChange: (e) => {
3115
+ setComment(e.target.value);
3116
+ }
3117
+ },
3118
+ `comment-teacher-for-${subjectData.student}-${subjectData.topic}`
3119
+ ),
3120
+ subjectData.status === "submitted" && /* @__PURE__ */ jsxs60(Fragment12, { children: [
3121
+ /* @__PURE__ */ jsx69(
3122
+ Button,
3123
+ {
3124
+ className: "-mb-1! mt-1! shadow-none!",
3125
+ text: "Ne pas valider la question",
3126
+ type: "button",
3127
+ onClick: rejectSubject,
3128
+ color: "sky"
3129
+ }
3130
+ ),
3131
+ /* @__PURE__ */ jsx69(
3132
+ Button,
3133
+ {
3134
+ className: "mb-1! mt-1! shadow-none!",
3135
+ text: "Valider la question",
3136
+ type: "button",
3137
+ onClick: approveSubject,
3138
+ color: "sky"
3139
+ }
3140
+ )
3141
+ ] })
3142
+ ] });
3143
+ }
3144
+
3145
+ // src/ui/support/support-form.tsx
3146
+ import { useEffect as useEffect15, useState as useState31 } from "react";
3147
+ import { cookiesGet as cookiesGet6, EMAIL_KEY, fetchCsrfClient as fetchCsrfClient6, getGoralysClientConfig as getGoralysClientConfig2, reasonsConfig, USERNAME_KEY as USERNAME_KEY3 } from "@goralys/core";
3148
+ import { jsx as jsx70, jsxs as jsxs61 } from "react/jsx-runtime";
3149
+ function SupportForm() {
3150
+ const { apiDomain, getSchoolToken } = getGoralysClientConfig2();
3151
+ const [loggedIn, setLoggedIn] = useState31(true);
3152
+ const [csrfToken, setCsrfToken] = useState31(null);
3153
+ const requestUrl = `${apiDomain}/support/contact?high-school-token=${getSchoolToken()}`;
3154
+ const [reason, setReason] = useState31("password-forgot");
3155
+ useEffect15(() => {
3156
+ const run = async () => {
3157
+ setCsrfToken(await fetchCsrfClient6("support-ticket"));
3158
+ setLoggedIn(!!cookiesGet6(USERNAME_KEY3));
3159
+ };
3160
+ run().then();
3161
+ }, []);
3162
+ return /* @__PURE__ */ jsxs61(Card, { className: "flex-col min-h-73 bg-sky-200 order-1 sm:order-2 h-fit", children: [
3163
+ /* @__PURE__ */ jsx70("h1", { className: "text-xl", children: "Contactez le support Goralys" }),
3164
+ /* @__PURE__ */ jsxs61("form", { className: "relative flex flex-col h-full", method: "POST", action: requestUrl, autoComplete: "on", children: [
3165
+ !loggedIn && /* @__PURE__ */ jsx70(
3166
+ FloatingInput,
3167
+ {
3168
+ id: "full-name",
3169
+ label: "Nom Complet",
3170
+ helper: "Ce nom sera transmit \xE0 notre \xE9quipe afin de vous identifi\xE9",
3171
+ autocomplete: "name",
3172
+ required: !loggedIn
3173
+ }
3174
+ ),
3175
+ /* @__PURE__ */ jsx70(
3176
+ FloatingInput,
3177
+ {
3178
+ id: "user-email",
3179
+ label: "Votre email",
3180
+ helper: "Votre email sera simplement utilis\xE9 pour vous r\xE9pondre.",
3181
+ defaultValue: cookiesGet6(EMAIL_KEY) ?? "",
3182
+ autocomplete: "email",
3183
+ required: true,
3184
+ email: true
3185
+ }
3186
+ ),
3187
+ /* @__PURE__ */ jsxs61("div", { className: "relative pb-0 mb-3", children: [
3188
+ /* @__PURE__ */ jsx70(
3189
+ "select",
3190
+ {
3191
+ className: "w-full border-0 border-b-2 border-sky-300 appearance-none\n cursor-pointer outline-none focus:ring-0 text-base leading-5\n text-heading pb-0 pr-5 subjects-search-select",
3192
+ value: reason,
3193
+ name: "raw-reason",
3194
+ onChange: (e) => setReason(e.target.value),
3195
+ children: Object.entries(reasonsConfig).map(([key, { label }]) => /* @__PURE__ */ jsx70("option", { value: key, children: label }, key))
3196
+ }
3197
+ ),
3198
+ /* @__PURE__ */ jsx70(
3199
+ "span",
3200
+ {
3201
+ className: "absolute bottom-0 left-0 w-0 h-0.5 bg-sky-500\n transition-all duration-300 ease-in-out\n peer-focus:w-full subjects-search-underline"
3202
+ }
3203
+ )
3204
+ ] }),
3205
+ /* @__PURE__ */ jsx70(TextArea, { maxLength: 250, id: "message", label: "Message", helper: "Veuillez d\xE9crire pr\xE9cis\xE9ment votre probl\xE8me.", required: true }),
3206
+ /* @__PURE__ */ jsx70("input", { type: "hidden", name: "csrf-token", value: (csrfToken ? csrfToken : "no-token").trim() }),
3207
+ /* @__PURE__ */ jsx70("input", { type: "hidden", name: "reason", value: reason }),
3208
+ /* @__PURE__ */ jsx70(Button, { type: "submit", text: "Envoyer mon message", className: "bottom-0 mt-2.5", disabled: !csrfToken })
3209
+ ] })
3210
+ ] });
3211
+ }
3212
+
3213
+ // src/ui/support/support-ticket-card.tsx
3214
+ import { buildApiUrl as buildApiUrl3, reasonsConfig as reasonsConfig2 } from "@goralys/core";
3215
+ import { jsx as jsx71, jsxs as jsxs62 } from "react/jsx-runtime";
3216
+ function SupportTicketCard({ ticket }) {
3217
+ return /* @__PURE__ */ jsxs62("div", { className: "h-fit w-200 flex flex-col bg-sky-200 gap-1 gap-y-3 p-1 mt-1", children: [
3218
+ /* @__PURE__ */ jsxs62("div", { className: "flex flex-row w-full justify-between", children: [
3219
+ /* @__PURE__ */ jsxs62("span", { children: [
3220
+ /* @__PURE__ */ jsxs62("strong", { children: [
3221
+ "#",
3222
+ ticket.id
3223
+ ] }),
3224
+ " [",
3225
+ reasonsConfig2[ticket.reason].label,
3226
+ "]"
3227
+ ] }),
3228
+ /* @__PURE__ */ jsx71(
3229
+ "a",
3230
+ {
3231
+ href: buildApiUrl3("ticket/contact", { t: ticket.id.toString() }, true),
3232
+ target: "_blank",
3233
+ title: "Contactez l'utilisateur",
3234
+ className: "cursor-pointer",
3235
+ children: /* @__PURE__ */ jsx71("strong", { children: ticket.opener })
3236
+ }
3237
+ )
3238
+ ] }),
3239
+ /* @__PURE__ */ jsx71(
3240
+ TextArea,
3241
+ {
3242
+ id: "message",
3243
+ label: "Message",
3244
+ defaultValue: ticket.message,
3245
+ disabled: true
3246
+ },
3247
+ "support-message-" + ticket.id + ticket.opener
3248
+ ),
3249
+ /* @__PURE__ */ jsx71(
3250
+ Button,
3251
+ {
3252
+ text: "Ce probl\xE8me a \xE9t\xE9 r\xE9solu",
3253
+ type: "button",
3254
+ onClick: () => {
3255
+ window.location.href = "/support/ticket?t=" + ticket.id;
3256
+ }
3257
+ }
3258
+ )
3259
+ ] });
3260
+ }
3261
+
3262
+ // src/ui/toast/flash-toast-listener.tsx
3263
+ import { useEffect as useEffect16, useLayoutEffect, useRef as useRef8 } from "react";
3264
+ import { buildApiUrl as buildApiUrl4, FLASH_TOAST_KEY as FLASH_TOAST_KEY2, goralysFetchClient as goralysFetchClient6, storageGet, storageRemove } from "@goralys/core";
3265
+ function FlashToastListener() {
3266
+ const { showToast } = useToast();
3267
+ const showToastRef = useRef8(showToast);
3268
+ useEffect16(() => {
3269
+ showToastRef.current = showToast;
3270
+ }, [showToast]);
3271
+ const pathname = useGoralysUI().usePathname();
3272
+ const showCachedToast = (destroyCache = true) => {
3273
+ const raw = storageGet(FLASH_TOAST_KEY2);
3274
+ if (!raw) return;
3275
+ if (destroyCache) storageRemove(FLASH_TOAST_KEY2);
3276
+ try {
3277
+ const parsed = JSON.parse(raw);
3278
+ if (!parsed.expires) return;
3279
+ const remaining = parsed.expires - Date.now();
3280
+ if (remaining <= 0) return;
3281
+ showToastRef.current(parsed, false, remaining);
3282
+ } catch {
3283
+ }
3284
+ };
3285
+ useLayoutEffect(() => showCachedToast(false), [pathname]);
3286
+ useEffect16(() => {
3287
+ let cancelled = false;
3288
+ const run = async () => {
3289
+ try {
3290
+ const res = await goralysFetchClient6("GET", buildApiUrl4("toast/flash", {}), void 0, {
3291
+ cache: "no-store"
3292
+ });
3293
+ const data = await res.json();
3294
+ if (data?.toast) {
3295
+ storageRemove(FLASH_TOAST_KEY2);
3296
+ showToastRef.current({
3297
+ type: data.toast.toastType,
3298
+ title: data.toast.toastTitle,
3299
+ message: data.toast.toastMessage
3300
+ });
3301
+ }
3302
+ if (cancelled) return;
3303
+ showCachedToast();
3304
+ } catch {
3305
+ if (!cancelled) showCachedToast();
3306
+ }
3307
+ };
3308
+ void run();
3309
+ return () => {
3310
+ cancelled = true;
3311
+ };
3312
+ }, [pathname]);
3313
+ return null;
3314
+ }
3315
+
3316
+ // src/ui/user/forms/login-form.tsx
3317
+ import { useEffect as useEffect17, useRef as useRef9, useState as useState32 } from "react";
3318
+ import { fetchCsrfClient as fetchCsrfClient7, getGoralysClientConfig as getGoralysClientConfig3 } from "@goralys/core";
3319
+ import { jsx as jsx72, jsxs as jsxs63 } from "react/jsx-runtime";
3320
+ function LoginForm() {
3321
+ const { apiDomain, getSchoolToken } = getGoralysClientConfig3();
3322
+ const isDesktop = useMediaQuery("(min-width: 640px)");
3323
+ const [csrfToken, setCsrfToken] = useState32(null);
3324
+ const [submitting, setSubmitting] = useState32(false);
3325
+ const submittingRef = useRef9(false);
3326
+ const requestUrl = `${apiDomain}/user/login?high-school-token=${getSchoolToken}`;
3327
+ useEffect17(() => {
3328
+ const canceled = false;
3329
+ fetchCsrfClient7("login").then((token) => {
3330
+ if (!canceled) setCsrfToken(token);
3331
+ }).catch(() => {
3332
+ if (!canceled) setCsrfToken(null);
3333
+ });
3334
+ }, []);
3335
+ function handleSubmit(e) {
3336
+ if (submittingRef.current) {
3337
+ e.preventDefault();
3338
+ return;
3339
+ }
3340
+ submittingRef.current = true;
3341
+ setSubmitting(true);
3342
+ }
3343
+ return /* @__PURE__ */ jsxs63(Card, { className: "relative flex-col h-65 bg-sky-200 order-1 sm:order-2", children: [
3344
+ /* @__PURE__ */ jsx72("h1", { className: "text-xl", children: "Connectez vous \xE0 votre compte Goralys" }),
3345
+ /* @__PURE__ */ jsxs63("form", { className: "relative flex flex-col h-full", action: requestUrl, method: "POST", autoComplete: "on", onSubmit: handleSubmit, children: [
3346
+ /* @__PURE__ */ jsx72(FloatingInput, { id: "username", label: "Identifiant", helper: "Identifiant au format p.nomX", autocomplete: "username", required: true }),
3347
+ /* @__PURE__ */ jsx72(FloatingInput, { id: "password", label: "Mot de passe", autocomplete: "current-password", password: true, required: true }),
3348
+ /* @__PURE__ */ jsx72("input", { type: "hidden", name: "csrf-token", value: (csrfToken ? csrfToken : "no-token").trim() }),
3349
+ /* @__PURE__ */ jsx72("input", { type: "hidden", name: "client_context", value: isDesktop ? "desktop" : "mobile" }),
3350
+ /* @__PURE__ */ jsx72(
3351
+ Button,
3352
+ {
3353
+ type: "submit",
3354
+ text: csrfToken === null ? "Chargement..." : submitting ? "Connexion..." : "Se connecter",
3355
+ className: "absolute! bottom-0",
3356
+ disabled: csrfToken === null || submitting
3357
+ }
3358
+ )
3359
+ ] })
3360
+ ] });
3361
+ }
3362
+
3363
+ // src/ui/user/forms/register-form.tsx
3364
+ import { useEffect as useEffect18, useRef as useRef10, useState as useState33 } from "react";
3365
+ import { fetchCsrfClient as fetchCsrfClient8, getGoralysClientConfig as getGoralysClientConfig4 } from "@goralys/core";
3366
+ import { jsx as jsx73, jsxs as jsxs64 } from "react/jsx-runtime";
3367
+ function RegisterForm() {
3368
+ const { apiDomain, getSchoolToken } = getGoralysClientConfig4();
3369
+ const searchParams = useGoralysUI().useSearchParams();
3370
+ const router = useGoralysUI().useRouter();
3371
+ const [id, setId] = useState33(void 0);
3372
+ const hasRun = useRef10(false);
3373
+ useEffect18(() => {
3374
+ if (hasRun.current) return;
3375
+ hasRun.current = true;
3376
+ const update = () => setId(searchParams.get("id") ?? void 0);
3377
+ if (!searchParams.get("id")) {
3378
+ router.replace("/user/register");
3379
+ return;
3380
+ }
3381
+ update();
3382
+ router.replace("/user/register");
3383
+ }, [searchParams, router]);
3384
+ const [csrfToken, setCsrfToken] = useState33(null);
3385
+ const requestUrl = `${apiDomain}/user/register?high-school-token=${getSchoolToken()}`;
3386
+ useEffect18(() => {
3387
+ const run = async () => setCsrfToken(await fetchCsrfClient8("register"));
3388
+ run().then();
3389
+ }, []);
3390
+ return /* @__PURE__ */ jsxs64(Card, { className: "flex-col h-79 bg-sky-200 order-1 sm:order-2", children: [
3391
+ /* @__PURE__ */ jsx73("h1", { className: "text-xl", children: "Cr\xE9ez votre compte chez Goralys" }),
3392
+ /* @__PURE__ */ jsxs64("form", { className: "relative flex flex-col h-full", method: "POST", action: requestUrl, autoComplete: "on", children: [
3393
+ /* @__PURE__ */ jsx73(
3394
+ FloatingInput,
3395
+ {
3396
+ id: "user-name",
3397
+ label: "Identifiant",
3398
+ helper: "Identifiant au format p.nomX.",
3399
+ defaultValue: id,
3400
+ autocomplete: "username",
3401
+ required: true
3402
+ }
3403
+ ),
3404
+ /* @__PURE__ */ jsx73(
3405
+ FloatingInput,
3406
+ {
3407
+ id: "password",
3408
+ label: "Mot de passe",
3409
+ helper: "Choisissez un mot de passe s\xE9curis\xE9.",
3410
+ autocomplete: "new-password",
3411
+ password: true,
3412
+ required: true
3413
+ }
3414
+ ),
3415
+ /* @__PURE__ */ jsx73(FloatingInput, { id: "email", label: "Addresse Mail", autocomplete: "email", helper: "Ce champ est optionnel.", email: true }),
3416
+ /* @__PURE__ */ jsx73("input", { type: "hidden", name: "csrf-token", value: (csrfToken ? csrfToken : "no-token").trim() }),
3417
+ /* @__PURE__ */ jsx73(
3418
+ Button,
3419
+ {
3420
+ type: "submit",
3421
+ text: csrfToken === null ? "Chargement..." : "Cr\xE9er mon compte",
3422
+ className: "absolute! bottom-0",
3423
+ disabled: csrfToken === null
3424
+ }
3425
+ )
3426
+ ] })
3427
+ ] });
3428
+ }
3429
+ export {
3430
+ AdminCard,
3431
+ AdminPanelCardSkeleton,
3432
+ AdminSubjectCardSkeleton,
3433
+ AdminSubjectsCard,
3434
+ AdminsSearchBar,
3435
+ AuditActionCard,
3436
+ AuditActionCardSkeleton,
3437
+ AuthTokenCard,
3438
+ Button,
3439
+ ButtonSkeleton,
3440
+ Card,
3441
+ CardTitle,
3442
+ Checkbox,
3443
+ CommentSkeleton,
3444
+ CommentStudent,
3445
+ CommentTeacher,
3446
+ ConfirmElement,
3447
+ ConfirmProvider,
3448
+ CookieBanner,
3449
+ CreateAdminElement,
3450
+ DraftModalElement,
3451
+ DraftModalProvider,
3452
+ Dropdown,
3453
+ DynamicNav,
3454
+ EmailModalElement,
3455
+ EmailModalProvider,
3456
+ FlashToastListener,
3457
+ FloatingInput,
3458
+ GoralysLogoLoader,
3459
+ GoralysUIProvider,
3460
+ ImportTopicsModalElement,
3461
+ ImportTopicsModalProvider,
3462
+ InputTextFile,
3463
+ InputZipFile,
3464
+ LoginForm,
3465
+ NavLink,
3466
+ PasswordModalElement,
3467
+ PasswordModalProvider,
3468
+ Providers,
3469
+ ReadonlyCard,
3470
+ RegisterForm,
3471
+ ReplaceTeacherElement,
3472
+ SideNav,
3473
+ StatusLegend,
3474
+ StudentCard,
3475
+ StudentCardSkeleton,
3476
+ SubjectInputAdmin,
3477
+ SubjectInputSkeleton,
3478
+ SubjectInputStudent,
3479
+ SubjectInputTeacher,
3480
+ SubjectStudentsDonut,
3481
+ SubjectTeachersDonut,
3482
+ SubjectTextArea,
3483
+ SubjectsFilter,
3484
+ SubjectsSearchBar,
3485
+ SupportForm,
3486
+ SupportTicketCard,
3487
+ SupportTicketCardSkeleton,
3488
+ TapBar,
3489
+ TapBarLink,
3490
+ TapBarUser,
3491
+ TeacherCard,
3492
+ TeacherCardSkeleton,
3493
+ TextArea,
3494
+ ToastElement,
3495
+ ToastProvider,
3496
+ UserCard,
3497
+ UserCardSkeleton,
3498
+ UserNav,
3499
+ UserSubjectsStudentCard,
3500
+ UserSubjectsStudentCardSkeleton,
3501
+ UserSubjectsTeacherCard,
3502
+ UserSubjectsTeacherCardSkeleton,
3503
+ UsersSearchBar,
3504
+ useAutoResize,
3505
+ useConfirm,
3506
+ useDraftModal,
3507
+ useEmailModal,
3508
+ useGoralysUI,
3509
+ useImportTopicsModal,
3510
+ useMediaQuery,
3511
+ useModalClose,
3512
+ usePasswordModal,
3513
+ useToast
3514
+ };