@capytale/meta-player 0.7.0 → 0.7.1
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 +305 -305
- 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 +154 -154
- 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 +32 -32
- package/src/features/activitySettings/store.ts +108 -105
- package/src/features/activitySettings/style.module.scss +5 -5
- package/src/features/activitySettings/types.ts +138 -138
- package/src/features/activitySettings/ui.tsx +295 -295
- 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 +153 -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 +272 -272
- package/src/features/navbar/capytale-menu/CountdownAndSaveButton.tsx +115 -115
- package/src/features/navbar/capytale-menu/index.tsx +256 -256
- 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 +124 -124
- 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 +350 -350
- package/src/features/pedago/SharedNotesEditor.tsx +144 -144
- package/src/features/pedago/index.tsx +204 -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,272 +1,272 @@
|
|
|
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
|
-
|
|
14
|
-
// TODO use https://capytale2.ac-paris.fr/vanilla/time-s.php
|
|
15
|
-
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/backend/capytale/clock.ts?ref_type=heads
|
|
16
|
-
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/api/time/clock.ts?ref_type=heads
|
|
17
|
-
|
|
18
|
-
const MS_PER_MINUTE = 1000 * 60;
|
|
19
|
-
const MS_PER_DAY = MS_PER_MINUTE * 60 * 24;
|
|
20
|
-
const MS_PER_YEAR = MS_PER_DAY * 365;
|
|
21
|
-
|
|
22
|
-
const Countdown: FC = () => {
|
|
23
|
-
const toast = useRef<Toast>(null); // Using its own toast because it may clear it
|
|
24
|
-
const mode = useAppSelector(selectMode);
|
|
25
|
-
const isDirty = useAppSelector(selectIsDirty);
|
|
26
|
-
const oneMinuteWarningShown = useRef(false);
|
|
27
|
-
const fifteenSecondsWarningShown = useRef(false);
|
|
28
|
-
const shouldSaveAt30s = useRef(true);
|
|
29
|
-
const save30sAttempted = useRef(false);
|
|
30
|
-
const zeroSecondsWarningShown = useRef(false);
|
|
31
|
-
const save = useSave();
|
|
32
|
-
|
|
33
|
-
const showOneMinuteWarning = () => {
|
|
34
|
-
oneMinuteWarningShown.current = true;
|
|
35
|
-
toast.current?.show({
|
|
36
|
-
severity: "warn",
|
|
37
|
-
summary: "Activité non sauvegardée",
|
|
38
|
-
detail: (
|
|
39
|
-
<>
|
|
40
|
-
<div>
|
|
41
|
-
Il vous reste moins d'une minute pour rendre votre activité. Elle
|
|
42
|
-
sera sauvegardée automatiquement 30 secondes avant la fin.
|
|
43
|
-
</div>
|
|
44
|
-
<div
|
|
45
|
-
style={{
|
|
46
|
-
display: "flex",
|
|
47
|
-
flexDirection: "column",
|
|
48
|
-
gap: "8px",
|
|
49
|
-
marginTop: "8px",
|
|
50
|
-
alignItems: "stretch",
|
|
51
|
-
}}
|
|
52
|
-
>
|
|
53
|
-
<Button
|
|
54
|
-
severity="danger"
|
|
55
|
-
onClick={() => {
|
|
56
|
-
shouldSaveAt30s.current = false;
|
|
57
|
-
toast.current?.clear();
|
|
58
|
-
}}
|
|
59
|
-
>
|
|
60
|
-
Ne pas sauvegarder (danger)
|
|
61
|
-
</Button>
|
|
62
|
-
<Button severity="success" onClick={save}>
|
|
63
|
-
Sauvegarder maintenant
|
|
64
|
-
</Button>
|
|
65
|
-
</div>
|
|
66
|
-
</>
|
|
67
|
-
),
|
|
68
|
-
sticky: true,
|
|
69
|
-
closable: false,
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const showFifteenSecondsWarning = () => {
|
|
74
|
-
fifteenSecondsWarningShown.current = true;
|
|
75
|
-
toast.current?.show({
|
|
76
|
-
severity: "error",
|
|
77
|
-
summary: "Activité non sauvegardée",
|
|
78
|
-
detail: (
|
|
79
|
-
<>
|
|
80
|
-
<div>
|
|
81
|
-
Il vous reste moins de 15 secondes pour rendre votre activité.
|
|
82
|
-
</div>
|
|
83
|
-
<div
|
|
84
|
-
style={{
|
|
85
|
-
display: "flex",
|
|
86
|
-
flexDirection: "column",
|
|
87
|
-
gap: "8px",
|
|
88
|
-
marginTop: "8px",
|
|
89
|
-
alignItems: "stretch",
|
|
90
|
-
}}
|
|
91
|
-
>
|
|
92
|
-
<Button
|
|
93
|
-
severity="danger"
|
|
94
|
-
onClick={() => {
|
|
95
|
-
save();
|
|
96
|
-
toast.current?.clear();
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
Sauvegarder maintenant
|
|
100
|
-
</Button>
|
|
101
|
-
</div>
|
|
102
|
-
</>
|
|
103
|
-
),
|
|
104
|
-
sticky: true,
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const activityJS = useActivityJS();
|
|
109
|
-
const deadlineMode =
|
|
110
|
-
activityJS.activitySession?.activityBunch.access_tr_mode.value;
|
|
111
|
-
const deadline =
|
|
112
|
-
!deadlineMode || deadlineMode === "none"
|
|
113
|
-
? null
|
|
114
|
-
: activityJS.activitySession?.activityBunch.access_timerange.value?.end;
|
|
115
|
-
|
|
116
|
-
const [diffMs, setDiffMs] = useState<number | null>(null);
|
|
117
|
-
|
|
118
|
-
const updateDiffMs = useCallback(() => {
|
|
119
|
-
if (!deadline) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const deadlineUTC = deadline.getTime();
|
|
123
|
-
const nowUTC = serverClock.now();
|
|
124
|
-
const newDiffMs = deadlineUTC - nowUTC;
|
|
125
|
-
setDiffMs(newDiffMs);
|
|
126
|
-
if (mode !== "assignment") {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (
|
|
130
|
-
newDiffMs != null &&
|
|
131
|
-
newDiffMs < MS_PER_MINUTE &&
|
|
132
|
-
newDiffMs > 35000 &&
|
|
133
|
-
isDirty &&
|
|
134
|
-
!oneMinuteWarningShown.current
|
|
135
|
-
) {
|
|
136
|
-
showOneMinuteWarning();
|
|
137
|
-
}
|
|
138
|
-
if (
|
|
139
|
-
newDiffMs != null &&
|
|
140
|
-
newDiffMs < 30000 &&
|
|
141
|
-
newDiffMs > 0 &&
|
|
142
|
-
shouldSaveAt30s.current &&
|
|
143
|
-
!save30sAttempted.current
|
|
144
|
-
) {
|
|
145
|
-
if (isDirty) {
|
|
146
|
-
save();
|
|
147
|
-
}
|
|
148
|
-
toast.current?.clear();
|
|
149
|
-
save30sAttempted.current = true;
|
|
150
|
-
}
|
|
151
|
-
if (
|
|
152
|
-
isDirty &&
|
|
153
|
-
newDiffMs != null &&
|
|
154
|
-
newDiffMs < 15000 &&
|
|
155
|
-
newDiffMs > 0 &&
|
|
156
|
-
!fifteenSecondsWarningShown.current
|
|
157
|
-
) {
|
|
158
|
-
showFifteenSecondsWarning();
|
|
159
|
-
}
|
|
160
|
-
if (newDiffMs <= 0 && !zeroSecondsWarningShown.current) {
|
|
161
|
-
zeroSecondsWarningShown.current = true;
|
|
162
|
-
toast.current?.clear();
|
|
163
|
-
toast.current?.show({
|
|
164
|
-
severity: "info",
|
|
165
|
-
summary: "Temps écoulé",
|
|
166
|
-
detail: "Le temps est écoulé, il n'est plus possible de sauvegarder.",
|
|
167
|
-
sticky: true,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}, [deadline, mode, isDirty]);
|
|
171
|
-
|
|
172
|
-
useInterval(updateDiffMs, 1000, !!deadline);
|
|
173
|
-
|
|
174
|
-
const displayed = useMemo<ReactNode>(() => {
|
|
175
|
-
if (!deadline || diffMs == null) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
if (diffMs >= MS_PER_DAY) {
|
|
179
|
-
return (
|
|
180
|
-
<div
|
|
181
|
-
aria-label="Deadline"
|
|
182
|
-
style={{
|
|
183
|
-
display: "flex",
|
|
184
|
-
alignItems: "center",
|
|
185
|
-
gap: "8px",
|
|
186
|
-
marginRight: "8px",
|
|
187
|
-
}}
|
|
188
|
-
>
|
|
189
|
-
<i className="pi pi-calendar-clock" />
|
|
190
|
-
<div
|
|
191
|
-
style={{
|
|
192
|
-
display: "flex",
|
|
193
|
-
flexDirection: "column",
|
|
194
|
-
alignItems: "center",
|
|
195
|
-
}}
|
|
196
|
-
>
|
|
197
|
-
{deadline
|
|
198
|
-
.toLocaleString("fr-FR", {
|
|
199
|
-
year: diffMs >= MS_PER_YEAR ? "numeric" : undefined,
|
|
200
|
-
month: "short",
|
|
201
|
-
day: "numeric",
|
|
202
|
-
hour: "numeric",
|
|
203
|
-
minute: "numeric",
|
|
204
|
-
})
|
|
205
|
-
.split(", ")
|
|
206
|
-
.map((s, i) => (
|
|
207
|
-
<div
|
|
208
|
-
key={i}
|
|
209
|
-
aria-label={i === 0 ? "Date limite" : "Heure limite"}
|
|
210
|
-
>
|
|
211
|
-
{s}
|
|
212
|
-
</div>
|
|
213
|
-
))}
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
if (diffMs <= 0) {
|
|
219
|
-
return (
|
|
220
|
-
<div
|
|
221
|
-
aria-label="Temps écoulé"
|
|
222
|
-
style={{
|
|
223
|
-
display: "flex",
|
|
224
|
-
alignItems: "center",
|
|
225
|
-
gap: "8px",
|
|
226
|
-
marginRight: "8px",
|
|
227
|
-
}}
|
|
228
|
-
>
|
|
229
|
-
<i className="pi pi-calendar-times" />
|
|
230
|
-
<span>Temps écoulé</span>
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
const diffSeconds = Math.floor(diffMs / 1000);
|
|
235
|
-
const seconds = diffSeconds % 60;
|
|
236
|
-
const minutes = Math.floor(diffSeconds / 60) % 60;
|
|
237
|
-
const hours = Math.floor(diffSeconds / 3600) % 24;
|
|
238
|
-
return (
|
|
239
|
-
<div
|
|
240
|
-
aria-label="Temps restant"
|
|
241
|
-
style={{
|
|
242
|
-
display: "flex",
|
|
243
|
-
alignItems: "center",
|
|
244
|
-
gap: "8px",
|
|
245
|
-
marginRight: "8px",
|
|
246
|
-
}}
|
|
247
|
-
>
|
|
248
|
-
<i className="pi pi-hourglass" />
|
|
249
|
-
{`${hours.toString().padStart(2, "0")}:${minutes
|
|
250
|
-
.toString()
|
|
251
|
-
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`}
|
|
252
|
-
</div>
|
|
253
|
-
);
|
|
254
|
-
}, [deadline, diffMs]);
|
|
255
|
-
|
|
256
|
-
if (!deadline) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return (
|
|
261
|
-
<div
|
|
262
|
-
style={{
|
|
263
|
-
userSelect: "none",
|
|
264
|
-
}}
|
|
265
|
-
>
|
|
266
|
-
{displayed}
|
|
267
|
-
<Toast position="bottom-right" ref={toast} />
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
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
|
+
|
|
14
|
+
// TODO use https://capytale2.ac-paris.fr/vanilla/time-s.php
|
|
15
|
+
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/backend/capytale/clock.ts?ref_type=heads
|
|
16
|
+
// https://forge.apps.education.fr/capytale/activity-js/-/blob/main/src/api/time/clock.ts?ref_type=heads
|
|
17
|
+
|
|
18
|
+
const MS_PER_MINUTE = 1000 * 60;
|
|
19
|
+
const MS_PER_DAY = MS_PER_MINUTE * 60 * 24;
|
|
20
|
+
const MS_PER_YEAR = MS_PER_DAY * 365;
|
|
21
|
+
|
|
22
|
+
const Countdown: FC = () => {
|
|
23
|
+
const toast = useRef<Toast>(null); // Using its own toast because it may clear it
|
|
24
|
+
const mode = useAppSelector(selectMode);
|
|
25
|
+
const isDirty = useAppSelector(selectIsDirty);
|
|
26
|
+
const oneMinuteWarningShown = useRef(false);
|
|
27
|
+
const fifteenSecondsWarningShown = useRef(false);
|
|
28
|
+
const shouldSaveAt30s = useRef(true);
|
|
29
|
+
const save30sAttempted = useRef(false);
|
|
30
|
+
const zeroSecondsWarningShown = useRef(false);
|
|
31
|
+
const save = useSave();
|
|
32
|
+
|
|
33
|
+
const showOneMinuteWarning = () => {
|
|
34
|
+
oneMinuteWarningShown.current = true;
|
|
35
|
+
toast.current?.show({
|
|
36
|
+
severity: "warn",
|
|
37
|
+
summary: "Activité non sauvegardée",
|
|
38
|
+
detail: (
|
|
39
|
+
<>
|
|
40
|
+
<div>
|
|
41
|
+
Il vous reste moins d'une minute pour rendre votre activité. Elle
|
|
42
|
+
sera sauvegardée automatiquement 30 secondes avant la fin.
|
|
43
|
+
</div>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
display: "flex",
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
gap: "8px",
|
|
49
|
+
marginTop: "8px",
|
|
50
|
+
alignItems: "stretch",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Button
|
|
54
|
+
severity="danger"
|
|
55
|
+
onClick={() => {
|
|
56
|
+
shouldSaveAt30s.current = false;
|
|
57
|
+
toast.current?.clear();
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
Ne pas sauvegarder (danger)
|
|
61
|
+
</Button>
|
|
62
|
+
<Button severity="success" onClick={save}>
|
|
63
|
+
Sauvegarder maintenant
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
</>
|
|
67
|
+
),
|
|
68
|
+
sticky: true,
|
|
69
|
+
closable: false,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const showFifteenSecondsWarning = () => {
|
|
74
|
+
fifteenSecondsWarningShown.current = true;
|
|
75
|
+
toast.current?.show({
|
|
76
|
+
severity: "error",
|
|
77
|
+
summary: "Activité non sauvegardée",
|
|
78
|
+
detail: (
|
|
79
|
+
<>
|
|
80
|
+
<div>
|
|
81
|
+
Il vous reste moins de 15 secondes pour rendre votre activité.
|
|
82
|
+
</div>
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
display: "flex",
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
gap: "8px",
|
|
88
|
+
marginTop: "8px",
|
|
89
|
+
alignItems: "stretch",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<Button
|
|
93
|
+
severity="danger"
|
|
94
|
+
onClick={() => {
|
|
95
|
+
save();
|
|
96
|
+
toast.current?.clear();
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
Sauvegarder maintenant
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
</>
|
|
103
|
+
),
|
|
104
|
+
sticky: true,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const activityJS = useActivityJS();
|
|
109
|
+
const deadlineMode =
|
|
110
|
+
activityJS.activitySession?.activityBunch.access_tr_mode.value;
|
|
111
|
+
const deadline =
|
|
112
|
+
!deadlineMode || deadlineMode === "none"
|
|
113
|
+
? null
|
|
114
|
+
: activityJS.activitySession?.activityBunch.access_timerange.value?.end;
|
|
115
|
+
|
|
116
|
+
const [diffMs, setDiffMs] = useState<number | null>(null);
|
|
117
|
+
|
|
118
|
+
const updateDiffMs = useCallback(() => {
|
|
119
|
+
if (!deadline) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const deadlineUTC = deadline.getTime();
|
|
123
|
+
const nowUTC = serverClock.now();
|
|
124
|
+
const newDiffMs = deadlineUTC - nowUTC;
|
|
125
|
+
setDiffMs(newDiffMs);
|
|
126
|
+
if (mode !== "assignment") {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
newDiffMs != null &&
|
|
131
|
+
newDiffMs < MS_PER_MINUTE &&
|
|
132
|
+
newDiffMs > 35000 &&
|
|
133
|
+
isDirty &&
|
|
134
|
+
!oneMinuteWarningShown.current
|
|
135
|
+
) {
|
|
136
|
+
showOneMinuteWarning();
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
newDiffMs != null &&
|
|
140
|
+
newDiffMs < 30000 &&
|
|
141
|
+
newDiffMs > 0 &&
|
|
142
|
+
shouldSaveAt30s.current &&
|
|
143
|
+
!save30sAttempted.current
|
|
144
|
+
) {
|
|
145
|
+
if (isDirty) {
|
|
146
|
+
save();
|
|
147
|
+
}
|
|
148
|
+
toast.current?.clear();
|
|
149
|
+
save30sAttempted.current = true;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
isDirty &&
|
|
153
|
+
newDiffMs != null &&
|
|
154
|
+
newDiffMs < 15000 &&
|
|
155
|
+
newDiffMs > 0 &&
|
|
156
|
+
!fifteenSecondsWarningShown.current
|
|
157
|
+
) {
|
|
158
|
+
showFifteenSecondsWarning();
|
|
159
|
+
}
|
|
160
|
+
if (newDiffMs <= 0 && !zeroSecondsWarningShown.current) {
|
|
161
|
+
zeroSecondsWarningShown.current = true;
|
|
162
|
+
toast.current?.clear();
|
|
163
|
+
toast.current?.show({
|
|
164
|
+
severity: "info",
|
|
165
|
+
summary: "Temps écoulé",
|
|
166
|
+
detail: "Le temps est écoulé, il n'est plus possible de sauvegarder.",
|
|
167
|
+
sticky: true,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}, [deadline, mode, isDirty]);
|
|
171
|
+
|
|
172
|
+
useInterval(updateDiffMs, 1000, !!deadline);
|
|
173
|
+
|
|
174
|
+
const displayed = useMemo<ReactNode>(() => {
|
|
175
|
+
if (!deadline || diffMs == null) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (diffMs >= MS_PER_DAY) {
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
aria-label="Deadline"
|
|
182
|
+
style={{
|
|
183
|
+
display: "flex",
|
|
184
|
+
alignItems: "center",
|
|
185
|
+
gap: "8px",
|
|
186
|
+
marginRight: "8px",
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<i className="pi pi-calendar-clock" />
|
|
190
|
+
<div
|
|
191
|
+
style={{
|
|
192
|
+
display: "flex",
|
|
193
|
+
flexDirection: "column",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{deadline
|
|
198
|
+
.toLocaleString("fr-FR", {
|
|
199
|
+
year: diffMs >= MS_PER_YEAR ? "numeric" : undefined,
|
|
200
|
+
month: "short",
|
|
201
|
+
day: "numeric",
|
|
202
|
+
hour: "numeric",
|
|
203
|
+
minute: "numeric",
|
|
204
|
+
})
|
|
205
|
+
.split(", ")
|
|
206
|
+
.map((s, i) => (
|
|
207
|
+
<div
|
|
208
|
+
key={i}
|
|
209
|
+
aria-label={i === 0 ? "Date limite" : "Heure limite"}
|
|
210
|
+
>
|
|
211
|
+
{s}
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (diffMs <= 0) {
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
aria-label="Temps écoulé"
|
|
222
|
+
style={{
|
|
223
|
+
display: "flex",
|
|
224
|
+
alignItems: "center",
|
|
225
|
+
gap: "8px",
|
|
226
|
+
marginRight: "8px",
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<i className="pi pi-calendar-times" />
|
|
230
|
+
<span>Temps écoulé</span>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const diffSeconds = Math.floor(diffMs / 1000);
|
|
235
|
+
const seconds = diffSeconds % 60;
|
|
236
|
+
const minutes = Math.floor(diffSeconds / 60) % 60;
|
|
237
|
+
const hours = Math.floor(diffSeconds / 3600) % 24;
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
aria-label="Temps restant"
|
|
241
|
+
style={{
|
|
242
|
+
display: "flex",
|
|
243
|
+
alignItems: "center",
|
|
244
|
+
gap: "8px",
|
|
245
|
+
marginRight: "8px",
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<i className="pi pi-hourglass" />
|
|
249
|
+
{`${hours.toString().padStart(2, "0")}:${minutes
|
|
250
|
+
.toString()
|
|
251
|
+
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}, [deadline, diffMs]);
|
|
255
|
+
|
|
256
|
+
if (!deadline) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
style={{
|
|
263
|
+
userSelect: "none",
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
{displayed}
|
|
267
|
+
<Toast position="bottom-right" ref={toast} />
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export default Countdown;
|