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