@capytale/meta-player 0.8.2 → 0.8.3
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/.prettierrc.json +4 -4
- package/LICENSE +674 -674
- package/README.md +12 -12
- package/eslint.config.js +28 -28
- package/index.html +15 -15
- package/package.json +60 -60
- package/public/themes/lara-dark-blue/theme.css +7015 -7015
- package/public/themes/lara-light-blue/theme.css +7005 -7005
- package/src/App.tsx +139 -139
- package/src/AppRedux.css +39 -39
- package/src/MetaPlayer.tsx +170 -170
- package/src/activityJs.ts +7 -7
- package/src/app/createAppSlice.ts +6 -6
- package/src/app/hooks.ts +12 -12
- package/src/app/store.ts +52 -52
- package/src/app.module.scss +80 -80
- package/src/demo.tsx +87 -87
- package/src/external/prime.ts +5 -5
- package/src/features/activityData/ExitWarning.ts +28 -28
- package/src/features/activityData/IsAntiCheatExitDetectionDisabledSetter.tsx +19 -19
- package/src/features/activityData/IsDirtySetter.tsx +17 -17
- package/src/features/activityData/MetaPlayerOptionsSetter.tsx +41 -41
- package/src/features/activityData/activityDataSlice.ts +256 -256
- package/src/features/activityData/activityJsData.ts +82 -82
- package/src/features/activityData/hooks.ts +34 -34
- package/src/features/activityData/metaPlayerOptions.ts +23 -23
- package/src/features/activityData/uiState.ts +20 -20
- package/src/features/activityJS/ActivityJSProvider.tsx +339 -339
- package/src/features/activityJS/AfterResetAction.tsx +23 -23
- package/src/features/activityJS/AfterSaveAction.tsx +23 -23
- package/src/features/activityJS/BeforeResetAction.tsx +23 -23
- package/src/features/activityJS/BeforeSaveAction.tsx +23 -23
- package/src/features/activityJS/Saver.tsx +167 -167
- package/src/features/activityJS/hooks.ts +85 -85
- package/src/features/activityJS/internal-hooks.ts +96 -96
- package/src/features/activityJS/saverSlice.ts +96 -96
- package/src/features/activitySettings/ActivitySettingsSetter.tsx +21 -21
- package/src/features/activitySettings/hooks.ts +12 -12
- package/src/features/activitySettings/index.tsx +43 -43
- package/src/features/activitySettings/store.ts +108 -108
- package/src/features/activitySettings/style.module.scss +8 -8
- package/src/features/activitySettings/types.ts +140 -140
- package/src/features/activitySettings/ui.tsx +299 -299
- package/src/features/functionalities/AttachedFilesFunctionality.ts +23 -23
- package/src/features/functionalities/PreviewDialog.tsx +83 -83
- package/src/features/functionalities/functionalitiesSlice.ts +98 -98
- package/src/features/functionalities/hooks.ts +70 -70
- package/src/features/layout/hooks.ts +7 -7
- package/src/features/layout/layoutSlice.ts +90 -90
- package/src/features/navbar/activity-info/index.tsx +54 -54
- package/src/features/navbar/activity-info/style.module.scss +38 -38
- package/src/features/navbar/activity-menu/ActivityQuickActions.tsx +57 -57
- package/src/features/navbar/activity-menu/index.tsx +154 -153
- package/src/features/navbar/activity-menu/style.module.scss +7 -7
- package/src/features/navbar/capytale-menu/CloneDialog.tsx +75 -75
- package/src/features/navbar/capytale-menu/Countdown.tsx +312 -312
- package/src/features/navbar/capytale-menu/CountdownAndSaveButton.tsx +115 -115
- package/src/features/navbar/capytale-menu/index.tsx +260 -260
- package/src/features/navbar/capytale-menu/style.module.scss +8 -8
- package/src/features/navbar/index.tsx +39 -39
- package/src/features/navbar/navbarSlice.ts +79 -79
- package/src/features/navbar/review-navbar/GradingNav.tsx +128 -128
- package/src/features/navbar/review-navbar/index.tsx +18 -18
- package/src/features/navbar/review-navbar/style.module.scss +22 -22
- package/src/features/navbar/sidebars/ActivitySidebarActions.tsx +57 -57
- package/src/features/navbar/sidebars/AttachedFilesSidebarContent.module.scss +43 -43
- package/src/features/navbar/sidebars/AttachedFilesSidebarContent.tsx +181 -181
- package/src/features/navbar/sidebars/SettingsSidebarContent.tsx +192 -192
- package/src/features/navbar/sidebars/style.module.scss +15 -15
- package/src/features/navbar/student-utils.ts +11 -11
- package/src/features/navbar/style.module.scss +65 -65
- package/src/features/pedago/InstructionsEditor.tsx +102 -102
- package/src/features/pedago/PdfEditor.tsx +91 -91
- package/src/features/pedago/PedagoCommands.tsx +353 -353
- package/src/features/pedago/SharedNotesEditor.tsx +144 -144
- package/src/features/pedago/index.tsx +207 -204
- package/src/features/pedago/style.module.scss +233 -233
- package/src/features/theming/ThemeSwitcher.tsx +51 -51
- package/src/features/theming/hooks.ts +6 -6
- package/src/features/theming/themingSlice.ts +93 -93
- package/src/features/toast.tsx +38 -38
- package/src/hooks/index.ts +16 -16
- package/src/index.css +132 -132
- package/src/index.ts +90 -90
- package/src/logo.svg +1 -1
- package/src/my_json_data.js +4146 -4146
- package/src/settings.ts +6 -6
- package/src/types.ts +9 -9
- package/src/utils/ButtonDoubleIcon.tsx +35 -35
- package/src/utils/CardSelector.tsx +41 -41
- package/src/utils/ErrorBoundary.tsx +41 -41
- package/src/utils/PopupButton.tsx +134 -134
- package/src/utils/activity.ts +8 -8
- package/src/utils/breakpoints.ts +4 -4
- package/src/utils/capytale.ts +10 -10
- package/src/utils/clipboard.ts +11 -11
- package/src/utils/download.ts +7 -7
- package/src/utils/equality.ts +32 -32
- package/src/utils/server-clock.ts +42 -42
- package/src/utils/style.module.scss +45 -45
- package/src/utils/useFullscreen.ts +65 -65
- package/src/vite-env.d.ts +1 -1
- package/tsconfig.json +28 -28
- package/tsconfig.node.json +9 -9
- package/vite.config.ts +11 -11
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
import { FC, ReactNode, useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { useInterval } from "primereact/hooks";
|
|
3
|
-
import { Toast } from "primereact/toast";
|
|
4
|
-
import { useActivityJS } from "../../activityJS/ActivityJSProvider";
|
|
5
|
-
import { useAppSelector } from "../../../app/hooks";
|
|
6
|
-
import {
|
|
7
|
-
selectIsDirty,
|
|
8
|
-
selectMode,
|
|
9
|
-
} from "../../activityData/activityDataSlice";
|
|
10
|
-
import { Button } from "primereact/button";
|
|
11
|
-
import { useSave } from "../../activityData/hooks";
|
|
12
|
-
import serverClock from "../../../utils/server-clock";
|
|
13
|
-
import { selectShowSaveForStudents } from "../../layout/layoutSlice";
|
|
14
|
-
|
|
15
|
-
// TODO use https://capytale2.ac-paris.fr/vanilla/time-s.php
|
|
16
|
-
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/backend/capytale/clock.ts?ref_type=heads
|
|
17
|
-
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/api/time/clock.ts?ref_type=heads
|
|
18
|
-
|
|
19
|
-
const MS_PER_MINUTE = 1000 * 60;
|
|
20
|
-
const MS_PER_DAY = MS_PER_MINUTE * 60 * 24;
|
|
21
|
-
const MS_PER_YEAR = MS_PER_DAY * 365;
|
|
22
|
-
|
|
23
|
-
const Countdown: FC = () => {
|
|
24
|
-
const toast = useRef<Toast>(null); // Using its own toast because it may clear it
|
|
25
|
-
const mode = useAppSelector(selectMode);
|
|
26
|
-
const isDirty = useAppSelector(selectIsDirty);
|
|
27
|
-
const showSaveForStudents = useAppSelector(selectShowSaveForStudents);
|
|
28
|
-
const showWarningsBecauseNoSave = useMemo(
|
|
29
|
-
() => mode === "assignment" && !showSaveForStudents,
|
|
30
|
-
[mode, showSaveForStudents],
|
|
31
|
-
);
|
|
32
|
-
const oneMinuteWarningShown = useRef(false);
|
|
33
|
-
const fifteenSecondsWarningShown = useRef(false);
|
|
34
|
-
const shouldSaveAt30s = useRef(true);
|
|
35
|
-
const save30sAttempted = useRef(false);
|
|
36
|
-
const zeroSecondsWarningShown = useRef(false);
|
|
37
|
-
const save = useSave();
|
|
38
|
-
|
|
39
|
-
const showOneMinuteWarning = useCallback(() => {
|
|
40
|
-
oneMinuteWarningShown.current = true;
|
|
41
|
-
toast.current?.show({
|
|
42
|
-
severity: "warn",
|
|
43
|
-
summary: showWarningsBecauseNoSave ? "Temps presque écoulé" : "Activité non sauvegardée",
|
|
44
|
-
detail: (
|
|
45
|
-
<>
|
|
46
|
-
<div>
|
|
47
|
-
{showWarningsBecauseNoSave ? (
|
|
48
|
-
<>Il vous reste moins d'une minute pour vérifier vos réponses.</>
|
|
49
|
-
) : (
|
|
50
|
-
<>
|
|
51
|
-
Il vous reste moins d'une minute pour enregistrer votre
|
|
52
|
-
activité. Elle sera sauvegardée automatiquement 30 secondes
|
|
53
|
-
avant la fin.
|
|
54
|
-
</>
|
|
55
|
-
)}
|
|
56
|
-
</div>
|
|
57
|
-
{!showWarningsBecauseNoSave && (
|
|
58
|
-
<div
|
|
59
|
-
style={{
|
|
60
|
-
display: "flex",
|
|
61
|
-
flexDirection: "column",
|
|
62
|
-
gap: "8px",
|
|
63
|
-
marginTop: "8px",
|
|
64
|
-
alignItems: "stretch",
|
|
65
|
-
}}
|
|
66
|
-
>
|
|
67
|
-
<Button
|
|
68
|
-
severity="danger"
|
|
69
|
-
onClick={() => {
|
|
70
|
-
shouldSaveAt30s.current = false;
|
|
71
|
-
toast.current?.clear();
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
Ne pas sauvegarder (danger)
|
|
75
|
-
</Button>
|
|
76
|
-
<Button severity="success" onClick={save}>
|
|
77
|
-
Sauvegarder maintenant
|
|
78
|
-
</Button>
|
|
79
|
-
</div>
|
|
80
|
-
)}
|
|
81
|
-
</>
|
|
82
|
-
),
|
|
83
|
-
sticky: true,
|
|
84
|
-
closable: false,
|
|
85
|
-
});
|
|
86
|
-
}, [save, showWarningsBecauseNoSave]);
|
|
87
|
-
|
|
88
|
-
const showFifteenSecondsWarning = useCallback(() => {
|
|
89
|
-
fifteenSecondsWarningShown.current = true;
|
|
90
|
-
toast.current?.show({
|
|
91
|
-
severity: "error",
|
|
92
|
-
summary: showWarningsBecauseNoSave ? "Temps presque écoulé" : "Activité non sauvegardée",
|
|
93
|
-
detail: (
|
|
94
|
-
<>
|
|
95
|
-
<div>
|
|
96
|
-
{showWarningsBecauseNoSave ? (
|
|
97
|
-
<>
|
|
98
|
-
Il vous reste moins de 15 secondes pour vérifier vos réponses.
|
|
99
|
-
</>
|
|
100
|
-
) : (
|
|
101
|
-
<>
|
|
102
|
-
Il vous reste moins de 15 secondes pour enregistrer votre
|
|
103
|
-
activité.
|
|
104
|
-
</>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
{!showWarningsBecauseNoSave && (
|
|
108
|
-
<div
|
|
109
|
-
style={{
|
|
110
|
-
display: "flex",
|
|
111
|
-
flexDirection: "column",
|
|
112
|
-
gap: "8px",
|
|
113
|
-
marginTop: "8px",
|
|
114
|
-
alignItems: "stretch",
|
|
115
|
-
}}
|
|
116
|
-
>
|
|
117
|
-
<Button
|
|
118
|
-
severity="danger"
|
|
119
|
-
onClick={() => {
|
|
120
|
-
save();
|
|
121
|
-
toast.current?.clear();
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
Sauvegarder maintenant
|
|
125
|
-
</Button>
|
|
126
|
-
</div>
|
|
127
|
-
)}
|
|
128
|
-
</>
|
|
129
|
-
),
|
|
130
|
-
sticky: true,
|
|
131
|
-
});
|
|
132
|
-
}, [save, showWarningsBecauseNoSave]);
|
|
133
|
-
|
|
134
|
-
const activityJS = useActivityJS();
|
|
135
|
-
const deadlineMode =
|
|
136
|
-
activityJS.activitySession?.activityBunch.access_tr_mode.value;
|
|
137
|
-
const deadline =
|
|
138
|
-
!deadlineMode || deadlineMode === "none"
|
|
139
|
-
? null
|
|
140
|
-
: activityJS.activitySession?.activityBunch.access_timerange.value?.end;
|
|
141
|
-
|
|
142
|
-
const [diffMs, setDiffMs] = useState<number | null>(null);
|
|
143
|
-
|
|
144
|
-
const updateDiffMs = useCallback(() => {
|
|
145
|
-
if (!deadline) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const deadlineUTC = deadline.getTime();
|
|
149
|
-
const nowUTC = serverClock.now();
|
|
150
|
-
const newDiffMs = deadlineUTC - nowUTC;
|
|
151
|
-
setDiffMs(newDiffMs);
|
|
152
|
-
if (mode !== "assignment") {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (
|
|
156
|
-
newDiffMs != null &&
|
|
157
|
-
newDiffMs < MS_PER_MINUTE &&
|
|
158
|
-
newDiffMs > 35000 &&
|
|
159
|
-
(isDirty || showWarningsBecauseNoSave) &&
|
|
160
|
-
!oneMinuteWarningShown.current
|
|
161
|
-
) {
|
|
162
|
-
showOneMinuteWarning();
|
|
163
|
-
}
|
|
164
|
-
if (
|
|
165
|
-
newDiffMs != null &&
|
|
166
|
-
newDiffMs < 30000 &&
|
|
167
|
-
newDiffMs > 0 &&
|
|
168
|
-
shouldSaveAt30s.current &&
|
|
169
|
-
!save30sAttempted.current
|
|
170
|
-
) {
|
|
171
|
-
if (isDirty) {
|
|
172
|
-
save();
|
|
173
|
-
}
|
|
174
|
-
toast.current?.clear();
|
|
175
|
-
save30sAttempted.current = true;
|
|
176
|
-
}
|
|
177
|
-
if (
|
|
178
|
-
(isDirty || showWarningsBecauseNoSave) &&
|
|
179
|
-
newDiffMs != null &&
|
|
180
|
-
newDiffMs < 15000 &&
|
|
181
|
-
newDiffMs > 0 &&
|
|
182
|
-
!fifteenSecondsWarningShown.current
|
|
183
|
-
) {
|
|
184
|
-
showFifteenSecondsWarning();
|
|
185
|
-
}
|
|
186
|
-
if (newDiffMs <= 0 && !zeroSecondsWarningShown.current) {
|
|
187
|
-
zeroSecondsWarningShown.current = true;
|
|
188
|
-
toast.current?.clear();
|
|
189
|
-
setTimeout(() => {
|
|
190
|
-
toast.current?.show({
|
|
191
|
-
severity: "info",
|
|
192
|
-
summary: "Temps écoulé",
|
|
193
|
-
detail: "Le temps est écoulé, il n'est plus possible de sauvegarder.",
|
|
194
|
-
sticky: true,
|
|
195
|
-
});
|
|
196
|
-
}, 500);
|
|
197
|
-
}
|
|
198
|
-
}, [deadline, mode, isDirty, showWarningsBecauseNoSave]);
|
|
199
|
-
|
|
200
|
-
useInterval(updateDiffMs, 1000, !!deadline);
|
|
201
|
-
|
|
202
|
-
const displayed = useMemo<ReactNode>(() => {
|
|
203
|
-
if (!deadline || diffMs == null) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
if (diffMs >= MS_PER_DAY) {
|
|
207
|
-
return (
|
|
208
|
-
<div
|
|
209
|
-
aria-label="Deadline"
|
|
210
|
-
style={{
|
|
211
|
-
display: "flex",
|
|
212
|
-
alignItems: "center",
|
|
213
|
-
gap: "8px",
|
|
214
|
-
marginRight: "8px",
|
|
215
|
-
}}
|
|
216
|
-
>
|
|
217
|
-
<i className="pi pi-calendar-clock" />
|
|
218
|
-
<div
|
|
219
|
-
style={{
|
|
220
|
-
display: "flex",
|
|
221
|
-
flexDirection: "column",
|
|
222
|
-
alignItems: "center",
|
|
223
|
-
}}
|
|
224
|
-
>
|
|
225
|
-
{deadline
|
|
226
|
-
.toLocaleString("fr-FR", {
|
|
227
|
-
year: diffMs >= MS_PER_YEAR ? "numeric" : undefined,
|
|
228
|
-
month: "short",
|
|
229
|
-
day: "numeric",
|
|
230
|
-
hour: "numeric",
|
|
231
|
-
minute: "numeric",
|
|
232
|
-
})
|
|
233
|
-
.split(", ")
|
|
234
|
-
.map((s, i) => (
|
|
235
|
-
<div
|
|
236
|
-
key={i}
|
|
237
|
-
aria-label={i === 0 ? "Date limite" : "Heure limite"}
|
|
238
|
-
>
|
|
239
|
-
{s}
|
|
240
|
-
</div>
|
|
241
|
-
))}
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (diffMs <= 0) {
|
|
247
|
-
return (
|
|
248
|
-
<div
|
|
249
|
-
aria-label="Temps écoulé"
|
|
250
|
-
style={{
|
|
251
|
-
display: "flex",
|
|
252
|
-
alignItems: "center",
|
|
253
|
-
gap: "8px",
|
|
254
|
-
marginRight: "8px",
|
|
255
|
-
background: "#ef4444",
|
|
256
|
-
borderRadius: "100px",
|
|
257
|
-
padding: "5px 10px",
|
|
258
|
-
fontWeight: "bold",
|
|
259
|
-
}}
|
|
260
|
-
>
|
|
261
|
-
<i className="pi pi-calendar-times" />
|
|
262
|
-
<span>Temps écoulé</span>
|
|
263
|
-
</div>
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
const diffSeconds = Math.floor(diffMs / 1000);
|
|
267
|
-
const seconds = diffSeconds % 60;
|
|
268
|
-
const minutes = Math.floor(diffSeconds / 60) % 60;
|
|
269
|
-
const hours = Math.floor(diffSeconds / 3600) % 24;
|
|
270
|
-
return (
|
|
271
|
-
<div
|
|
272
|
-
aria-label="Temps restant"
|
|
273
|
-
style={{
|
|
274
|
-
display: "flex",
|
|
275
|
-
alignItems: "center",
|
|
276
|
-
gap: "8px",
|
|
277
|
-
marginRight: "8px",
|
|
278
|
-
...(hours === 0 && minutes < 5
|
|
279
|
-
? {
|
|
280
|
-
background: "#ef4444",
|
|
281
|
-
borderRadius: "100px",
|
|
282
|
-
padding: "5px 10px",
|
|
283
|
-
fontWeight: "bold",
|
|
284
|
-
}
|
|
285
|
-
: {}),
|
|
286
|
-
}}
|
|
287
|
-
>
|
|
288
|
-
<i className="pi pi-hourglass" />
|
|
289
|
-
{`${hours.toString().padStart(2, "0")}:${minutes
|
|
290
|
-
.toString()
|
|
291
|
-
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`}
|
|
292
|
-
</div>
|
|
293
|
-
);
|
|
294
|
-
}, [deadline, diffMs]);
|
|
295
|
-
|
|
296
|
-
if (!deadline) {
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<div
|
|
302
|
-
style={{
|
|
303
|
-
userSelect: "none",
|
|
304
|
-
}}
|
|
305
|
-
>
|
|
306
|
-
{displayed}
|
|
307
|
-
<Toast position="bottom-right" ref={toast} />
|
|
308
|
-
</div>
|
|
309
|
-
);
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
export default Countdown;
|
|
1
|
+
import { FC, ReactNode, useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useInterval } from "primereact/hooks";
|
|
3
|
+
import { Toast } from "primereact/toast";
|
|
4
|
+
import { useActivityJS } from "../../activityJS/ActivityJSProvider";
|
|
5
|
+
import { useAppSelector } from "../../../app/hooks";
|
|
6
|
+
import {
|
|
7
|
+
selectIsDirty,
|
|
8
|
+
selectMode,
|
|
9
|
+
} from "../../activityData/activityDataSlice";
|
|
10
|
+
import { Button } from "primereact/button";
|
|
11
|
+
import { useSave } from "../../activityData/hooks";
|
|
12
|
+
import serverClock from "../../../utils/server-clock";
|
|
13
|
+
import { selectShowSaveForStudents } from "../../layout/layoutSlice";
|
|
14
|
+
|
|
15
|
+
// TODO use https://capytale2.ac-paris.fr/vanilla/time-s.php
|
|
16
|
+
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/backend/capytale/clock.ts?ref_type=heads
|
|
17
|
+
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/api/time/clock.ts?ref_type=heads
|
|
18
|
+
|
|
19
|
+
const MS_PER_MINUTE = 1000 * 60;
|
|
20
|
+
const MS_PER_DAY = MS_PER_MINUTE * 60 * 24;
|
|
21
|
+
const MS_PER_YEAR = MS_PER_DAY * 365;
|
|
22
|
+
|
|
23
|
+
const Countdown: FC = () => {
|
|
24
|
+
const toast = useRef<Toast>(null); // Using its own toast because it may clear it
|
|
25
|
+
const mode = useAppSelector(selectMode);
|
|
26
|
+
const isDirty = useAppSelector(selectIsDirty);
|
|
27
|
+
const showSaveForStudents = useAppSelector(selectShowSaveForStudents);
|
|
28
|
+
const showWarningsBecauseNoSave = useMemo(
|
|
29
|
+
() => mode === "assignment" && !showSaveForStudents,
|
|
30
|
+
[mode, showSaveForStudents],
|
|
31
|
+
);
|
|
32
|
+
const oneMinuteWarningShown = useRef(false);
|
|
33
|
+
const fifteenSecondsWarningShown = useRef(false);
|
|
34
|
+
const shouldSaveAt30s = useRef(true);
|
|
35
|
+
const save30sAttempted = useRef(false);
|
|
36
|
+
const zeroSecondsWarningShown = useRef(false);
|
|
37
|
+
const save = useSave();
|
|
38
|
+
|
|
39
|
+
const showOneMinuteWarning = useCallback(() => {
|
|
40
|
+
oneMinuteWarningShown.current = true;
|
|
41
|
+
toast.current?.show({
|
|
42
|
+
severity: "warn",
|
|
43
|
+
summary: showWarningsBecauseNoSave ? "Temps presque écoulé" : "Activité non sauvegardée",
|
|
44
|
+
detail: (
|
|
45
|
+
<>
|
|
46
|
+
<div>
|
|
47
|
+
{showWarningsBecauseNoSave ? (
|
|
48
|
+
<>Il vous reste moins d'une minute pour vérifier vos réponses.</>
|
|
49
|
+
) : (
|
|
50
|
+
<>
|
|
51
|
+
Il vous reste moins d'une minute pour enregistrer votre
|
|
52
|
+
activité. Elle sera sauvegardée automatiquement 30 secondes
|
|
53
|
+
avant la fin.
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
{!showWarningsBecauseNoSave && (
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
display: "flex",
|
|
61
|
+
flexDirection: "column",
|
|
62
|
+
gap: "8px",
|
|
63
|
+
marginTop: "8px",
|
|
64
|
+
alignItems: "stretch",
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Button
|
|
68
|
+
severity="danger"
|
|
69
|
+
onClick={() => {
|
|
70
|
+
shouldSaveAt30s.current = false;
|
|
71
|
+
toast.current?.clear();
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
Ne pas sauvegarder (danger)
|
|
75
|
+
</Button>
|
|
76
|
+
<Button severity="success" onClick={save}>
|
|
77
|
+
Sauvegarder maintenant
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</>
|
|
82
|
+
),
|
|
83
|
+
sticky: true,
|
|
84
|
+
closable: false,
|
|
85
|
+
});
|
|
86
|
+
}, [save, showWarningsBecauseNoSave]);
|
|
87
|
+
|
|
88
|
+
const showFifteenSecondsWarning = useCallback(() => {
|
|
89
|
+
fifteenSecondsWarningShown.current = true;
|
|
90
|
+
toast.current?.show({
|
|
91
|
+
severity: "error",
|
|
92
|
+
summary: showWarningsBecauseNoSave ? "Temps presque écoulé" : "Activité non sauvegardée",
|
|
93
|
+
detail: (
|
|
94
|
+
<>
|
|
95
|
+
<div>
|
|
96
|
+
{showWarningsBecauseNoSave ? (
|
|
97
|
+
<>
|
|
98
|
+
Il vous reste moins de 15 secondes pour vérifier vos réponses.
|
|
99
|
+
</>
|
|
100
|
+
) : (
|
|
101
|
+
<>
|
|
102
|
+
Il vous reste moins de 15 secondes pour enregistrer votre
|
|
103
|
+
activité.
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{!showWarningsBecauseNoSave && (
|
|
108
|
+
<div
|
|
109
|
+
style={{
|
|
110
|
+
display: "flex",
|
|
111
|
+
flexDirection: "column",
|
|
112
|
+
gap: "8px",
|
|
113
|
+
marginTop: "8px",
|
|
114
|
+
alignItems: "stretch",
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<Button
|
|
118
|
+
severity="danger"
|
|
119
|
+
onClick={() => {
|
|
120
|
+
save();
|
|
121
|
+
toast.current?.clear();
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
Sauvegarder maintenant
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</>
|
|
129
|
+
),
|
|
130
|
+
sticky: true,
|
|
131
|
+
});
|
|
132
|
+
}, [save, showWarningsBecauseNoSave]);
|
|
133
|
+
|
|
134
|
+
const activityJS = useActivityJS();
|
|
135
|
+
const deadlineMode =
|
|
136
|
+
activityJS.activitySession?.activityBunch.access_tr_mode.value;
|
|
137
|
+
const deadline =
|
|
138
|
+
!deadlineMode || deadlineMode === "none"
|
|
139
|
+
? null
|
|
140
|
+
: activityJS.activitySession?.activityBunch.access_timerange.value?.end;
|
|
141
|
+
|
|
142
|
+
const [diffMs, setDiffMs] = useState<number | null>(null);
|
|
143
|
+
|
|
144
|
+
const updateDiffMs = useCallback(() => {
|
|
145
|
+
if (!deadline) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const deadlineUTC = deadline.getTime();
|
|
149
|
+
const nowUTC = serverClock.now();
|
|
150
|
+
const newDiffMs = deadlineUTC - nowUTC;
|
|
151
|
+
setDiffMs(newDiffMs);
|
|
152
|
+
if (mode !== "assignment") {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
newDiffMs != null &&
|
|
157
|
+
newDiffMs < MS_PER_MINUTE &&
|
|
158
|
+
newDiffMs > 35000 &&
|
|
159
|
+
(isDirty || showWarningsBecauseNoSave) &&
|
|
160
|
+
!oneMinuteWarningShown.current
|
|
161
|
+
) {
|
|
162
|
+
showOneMinuteWarning();
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
newDiffMs != null &&
|
|
166
|
+
newDiffMs < 30000 &&
|
|
167
|
+
newDiffMs > 0 &&
|
|
168
|
+
shouldSaveAt30s.current &&
|
|
169
|
+
!save30sAttempted.current
|
|
170
|
+
) {
|
|
171
|
+
if (isDirty) {
|
|
172
|
+
save();
|
|
173
|
+
}
|
|
174
|
+
toast.current?.clear();
|
|
175
|
+
save30sAttempted.current = true;
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
(isDirty || showWarningsBecauseNoSave) &&
|
|
179
|
+
newDiffMs != null &&
|
|
180
|
+
newDiffMs < 15000 &&
|
|
181
|
+
newDiffMs > 0 &&
|
|
182
|
+
!fifteenSecondsWarningShown.current
|
|
183
|
+
) {
|
|
184
|
+
showFifteenSecondsWarning();
|
|
185
|
+
}
|
|
186
|
+
if (newDiffMs <= 0 && !zeroSecondsWarningShown.current) {
|
|
187
|
+
zeroSecondsWarningShown.current = true;
|
|
188
|
+
toast.current?.clear();
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
toast.current?.show({
|
|
191
|
+
severity: "info",
|
|
192
|
+
summary: "Temps écoulé",
|
|
193
|
+
detail: "Le temps est écoulé, il n'est plus possible de sauvegarder.",
|
|
194
|
+
sticky: true,
|
|
195
|
+
});
|
|
196
|
+
}, 500);
|
|
197
|
+
}
|
|
198
|
+
}, [deadline, mode, isDirty, showWarningsBecauseNoSave]);
|
|
199
|
+
|
|
200
|
+
useInterval(updateDiffMs, 1000, !!deadline);
|
|
201
|
+
|
|
202
|
+
const displayed = useMemo<ReactNode>(() => {
|
|
203
|
+
if (!deadline || diffMs == null) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (diffMs >= MS_PER_DAY) {
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
aria-label="Deadline"
|
|
210
|
+
style={{
|
|
211
|
+
display: "flex",
|
|
212
|
+
alignItems: "center",
|
|
213
|
+
gap: "8px",
|
|
214
|
+
marginRight: "8px",
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<i className="pi pi-calendar-clock" />
|
|
218
|
+
<div
|
|
219
|
+
style={{
|
|
220
|
+
display: "flex",
|
|
221
|
+
flexDirection: "column",
|
|
222
|
+
alignItems: "center",
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
{deadline
|
|
226
|
+
.toLocaleString("fr-FR", {
|
|
227
|
+
year: diffMs >= MS_PER_YEAR ? "numeric" : undefined,
|
|
228
|
+
month: "short",
|
|
229
|
+
day: "numeric",
|
|
230
|
+
hour: "numeric",
|
|
231
|
+
minute: "numeric",
|
|
232
|
+
})
|
|
233
|
+
.split(", ")
|
|
234
|
+
.map((s, i) => (
|
|
235
|
+
<div
|
|
236
|
+
key={i}
|
|
237
|
+
aria-label={i === 0 ? "Date limite" : "Heure limite"}
|
|
238
|
+
>
|
|
239
|
+
{s}
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (diffMs <= 0) {
|
|
247
|
+
return (
|
|
248
|
+
<div
|
|
249
|
+
aria-label="Temps écoulé"
|
|
250
|
+
style={{
|
|
251
|
+
display: "flex",
|
|
252
|
+
alignItems: "center",
|
|
253
|
+
gap: "8px",
|
|
254
|
+
marginRight: "8px",
|
|
255
|
+
background: "#ef4444",
|
|
256
|
+
borderRadius: "100px",
|
|
257
|
+
padding: "5px 10px",
|
|
258
|
+
fontWeight: "bold",
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
<i className="pi pi-calendar-times" />
|
|
262
|
+
<span>Temps écoulé</span>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const diffSeconds = Math.floor(diffMs / 1000);
|
|
267
|
+
const seconds = diffSeconds % 60;
|
|
268
|
+
const minutes = Math.floor(diffSeconds / 60) % 60;
|
|
269
|
+
const hours = Math.floor(diffSeconds / 3600) % 24;
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
aria-label="Temps restant"
|
|
273
|
+
style={{
|
|
274
|
+
display: "flex",
|
|
275
|
+
alignItems: "center",
|
|
276
|
+
gap: "8px",
|
|
277
|
+
marginRight: "8px",
|
|
278
|
+
...(hours === 0 && minutes < 5
|
|
279
|
+
? {
|
|
280
|
+
background: "#ef4444",
|
|
281
|
+
borderRadius: "100px",
|
|
282
|
+
padding: "5px 10px",
|
|
283
|
+
fontWeight: "bold",
|
|
284
|
+
}
|
|
285
|
+
: {}),
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<i className="pi pi-hourglass" />
|
|
289
|
+
{`${hours.toString().padStart(2, "0")}:${minutes
|
|
290
|
+
.toString()
|
|
291
|
+
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}, [deadline, diffMs]);
|
|
295
|
+
|
|
296
|
+
if (!deadline) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
style={{
|
|
303
|
+
userSelect: "none",
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
{displayed}
|
|
307
|
+
<Toast position="bottom-right" ref={toast} />
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default Countdown;
|