@capytale/meta-player 0.0.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.
Files changed (66) hide show
  1. package/.eslintrc.json +35 -0
  2. package/.prettierrc.json +4 -0
  3. package/README.md +27 -0
  4. package/index.html +15 -0
  5. package/package.json +48 -0
  6. package/public/themes/lara-dark-blue/fonts/Inter-italic.var.woff2 +0 -0
  7. package/public/themes/lara-dark-blue/fonts/Inter-roman.var.woff2 +0 -0
  8. package/public/themes/lara-dark-blue/theme.css +7015 -0
  9. package/public/themes/lara-light-blue/fonts/Inter-italic.var.woff2 +0 -0
  10. package/public/themes/lara-light-blue/fonts/Inter-roman.var.woff2 +0 -0
  11. package/public/themes/lara-light-blue/theme.css +7005 -0
  12. package/src/App.tsx +116 -0
  13. package/src/AppRedux.css +39 -0
  14. package/src/MetaPlayer.tsx +51 -0
  15. package/src/app/createAppSlice.ts +6 -0
  16. package/src/app/hooks.ts +12 -0
  17. package/src/app/store.ts +46 -0
  18. package/src/app.module.scss +56 -0
  19. package/src/demo.tsx +81 -0
  20. package/src/features/activityData/IsDirtySetter.tsx +17 -0
  21. package/src/features/activityData/OptionSetter.tsx +35 -0
  22. package/src/features/activityData/activityDataSlice.ts +250 -0
  23. package/src/features/activityData/metaPlayerOptions.ts +17 -0
  24. package/src/features/activityJS/ActivityJSProvider.tsx +110 -0
  25. package/src/features/activityJS/AfterSaveAction.tsx +23 -0
  26. package/src/features/activityJS/BeforeSaveAction.tsx +23 -0
  27. package/src/features/activityJS/Saver.tsx +147 -0
  28. package/src/features/activityJS/hooks.ts +93 -0
  29. package/src/features/activityJS/saverSlice.ts +58 -0
  30. package/src/features/layout/layoutSlice.ts +76 -0
  31. package/src/features/navbar/ActivityInfo.tsx +41 -0
  32. package/src/features/navbar/ActivityMenu.tsx +56 -0
  33. package/src/features/navbar/ActivityQuickActions.tsx +52 -0
  34. package/src/features/navbar/ActivitySidebarActions.tsx +52 -0
  35. package/src/features/navbar/CapytaleMenu.tsx +183 -0
  36. package/src/features/navbar/GradingNav.tsx +120 -0
  37. package/src/features/navbar/ReviewNavbar.tsx +18 -0
  38. package/src/features/navbar/SidebarContent.tsx +125 -0
  39. package/src/features/navbar/index.tsx +33 -0
  40. package/src/features/navbar/navbarSlice.ts +51 -0
  41. package/src/features/navbar/student-utils.ts +11 -0
  42. package/src/features/navbar/style.module.scss +162 -0
  43. package/src/features/pedago/AnswerSheetEditor.tsx +65 -0
  44. package/src/features/pedago/InstructionsEditor.tsx +82 -0
  45. package/src/features/pedago/index.tsx +219 -0
  46. package/src/features/pedago/style.module.scss +104 -0
  47. package/src/features/theming/ThemeSwitcher.tsx +51 -0
  48. package/src/features/theming/themingSlice.ts +93 -0
  49. package/src/hooks/index.ts +8 -0
  50. package/src/index.css +30 -0
  51. package/src/index.tsx +6 -0
  52. package/src/logo.svg +1 -0
  53. package/src/my_json_data.js +4146 -0
  54. package/src/settings.ts +6 -0
  55. package/src/setupTests.ts +1 -0
  56. package/src/utils/ErrorBoundary.tsx +41 -0
  57. package/src/utils/PopupButton.tsx +135 -0
  58. package/src/utils/activity.ts +8 -0
  59. package/src/utils/breakpoints.ts +5 -0
  60. package/src/utils/clipboard.ts +11 -0
  61. package/src/utils/test-utils.tsx +65 -0
  62. package/src/utils/useFullscreen.ts +65 -0
  63. package/src/vite-env.d.ts +1 -0
  64. package/tsconfig.json +27 -0
  65. package/tsconfig.node.json +9 -0
  66. package/vite.config.ts +17 -0
@@ -0,0 +1,125 @@
1
+ import { FC } from "react";
2
+
3
+ import { Fieldset } from "primereact/fieldset";
4
+
5
+ import { selectOrientation, setLayout } from "../layout/layoutSlice";
6
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
7
+ import { RadioButton } from "primereact/radiobutton";
8
+
9
+ import styles from "./style.module.scss";
10
+ import {
11
+ selectThemeNameOrAuto,
12
+ setAutoSwitch,
13
+ switchToTheme,
14
+ } from "../theming/themingSlice";
15
+ import { selectSidebarActions } from "./navbarSlice";
16
+ import ActivitySidebarActions from "./ActivitySidebarActions";
17
+ import { classNames } from "primereact/utils";
18
+ import {
19
+ selectCanChoosePedagoLayout,
20
+ selectCanChooseTheme,
21
+ } from "../activityData/activityDataSlice";
22
+
23
+ const SidebarContent: FC = () => {
24
+ const canChooseOrientation = useAppSelector(selectCanChoosePedagoLayout);
25
+ const canChooseTheme = useAppSelector(selectCanChooseTheme);
26
+ const orientation = useAppSelector(selectOrientation);
27
+ const themeName = useAppSelector(selectThemeNameOrAuto);
28
+ const sidebarActions = useAppSelector(selectSidebarActions);
29
+ const dispatch = useAppDispatch();
30
+ return (
31
+ <>
32
+ {sidebarActions.length > 0 && (
33
+ <Fieldset
34
+ legend="Actions"
35
+ className={classNames(
36
+ styles.sidebarFieldset,
37
+ styles.sidebarFieldsetButtons,
38
+ )}
39
+ >
40
+ <ActivitySidebarActions />
41
+ </Fieldset>
42
+ )}
43
+ {canChooseOrientation && (
44
+ <Fieldset legend="Disposition" className={styles.sidebarFieldset}>
45
+ <div className={styles.sidebarRadioButtons}>
46
+ <div className={styles.sidebarRadioGroup}>
47
+ <RadioButton
48
+ inputId="rb-horizontal"
49
+ name="horizontal"
50
+ value="horizontal"
51
+ onChange={(e) => dispatch(setLayout(e.value))}
52
+ checked={orientation === "horizontal"}
53
+ />
54
+ <label htmlFor="rb-horizontal" className="ml-2">
55
+ Horizontale
56
+ </label>
57
+ </div>
58
+ <div className={styles.sidebarRadioGroup}>
59
+ <RadioButton
60
+ inputId="rb-vertical"
61
+ name="vertical"
62
+ value="vertical"
63
+ onChange={(e) => dispatch(setLayout(e.value))}
64
+ checked={orientation === "vertical"}
65
+ />
66
+ <label htmlFor="rb-vertical" className="ml-2">
67
+ Verticale
68
+ </label>
69
+ </div>
70
+ </div>
71
+ </Fieldset>
72
+ )}
73
+ {canChooseTheme && (
74
+ <Fieldset legend="Thème" className={styles.sidebarFieldset}>
75
+ <div className={styles.sidebarRadioButtons}>
76
+ <div className={styles.sidebarRadioGroup}>
77
+ <RadioButton
78
+ inputId="rb-light"
79
+ name="light"
80
+ value="light"
81
+ onChange={(_e) => {
82
+ dispatch(setAutoSwitch(false));
83
+ dispatch(switchToTheme("light"));
84
+ }}
85
+ checked={themeName === "Clair"}
86
+ />
87
+ <label htmlFor="rb-light" className="ml-2">
88
+ Clair
89
+ </label>
90
+ </div>
91
+ <div className={styles.sidebarRadioGroup}>
92
+ <RadioButton
93
+ inputId="rb-dark"
94
+ name="dark"
95
+ value="dark"
96
+ onChange={(_e) => {
97
+ dispatch(setAutoSwitch(false));
98
+ dispatch(switchToTheme("dark"));
99
+ }}
100
+ checked={themeName === "Sombre"}
101
+ />
102
+ <label htmlFor="rb-dark" className="ml-2">
103
+ Sombre
104
+ </label>
105
+ </div>
106
+ <div className={styles.sidebarRadioGroup}>
107
+ <RadioButton
108
+ inputId="rb-auto"
109
+ name="auto"
110
+ value="auto"
111
+ onChange={(_e) => dispatch(setAutoSwitch(true))}
112
+ checked={themeName === "Système (auto)"}
113
+ />
114
+ <label htmlFor="rb-auto" className="ml-2">
115
+ Système (auto)
116
+ </label>
117
+ </div>
118
+ </div>
119
+ </Fieldset>
120
+ )}
121
+ </>
122
+ );
123
+ };
124
+
125
+ export default SidebarContent;
@@ -0,0 +1,33 @@
1
+ import { MD } from "../../utils/breakpoints";
2
+ import { useWindowSize } from "@uidotdev/usehooks";
3
+ import ActivityInfo from "./ActivityInfo";
4
+ import ActivityMenu from "./ActivityMenu";
5
+ import CapytaleMenu from "./CapytaleMenu";
6
+ import styles from "./style.module.scss";
7
+ import { useMemo } from "react";
8
+ import { useAppSelector } from "../../app/hooks";
9
+ import { selectReturnUrl } from "../activityData/activityDataSlice";
10
+
11
+ const Navbar: React.FC = () => {
12
+ const returnUrl = useAppSelector(selectReturnUrl);
13
+ const windowsSize = useWindowSize();
14
+ const isSmall = useMemo(
15
+ () => windowsSize.width && windowsSize.width < MD,
16
+ [windowsSize.width],
17
+ );
18
+ const logoText = isSmall ? "C" : "CAPYTALE";
19
+ return (
20
+ <div className={styles.navbar}>
21
+ <div className={styles.navbarContainer}>
22
+ <div className={styles.navbarLogo}>
23
+ <a href={returnUrl}>{logoText}</a>
24
+ </div>
25
+ <CapytaleMenu />
26
+ <ActivityInfo />
27
+ <ActivityMenu />
28
+ </div>
29
+ </div>
30
+ );
31
+ };
32
+
33
+ export default Navbar;
@@ -0,0 +1,51 @@
1
+ import type { PayloadAction } from "@reduxjs/toolkit";
2
+ import { createAppSlice } from "../../app/createAppSlice";
3
+
4
+ export type QuickAction = {
5
+ title: string;
6
+ icon: string;
7
+ action: () => void;
8
+ };
9
+
10
+ export interface NavbarState {
11
+ quickActions: QuickAction[];
12
+ sidebarActions: QuickAction[];
13
+ }
14
+
15
+ const initialState: NavbarState = {
16
+ quickActions: [],
17
+ sidebarActions: [],
18
+ };
19
+
20
+ // If you are not using async thunks you can use the standalone `createSlice`.
21
+ export const navbarSlice = createAppSlice({
22
+ name: "navbar",
23
+ // `createSlice` will infer the state type from the `initialState` argument
24
+ initialState,
25
+ // The `reducers` field lets us define reducers and generate associated actions
26
+ reducers: (create) => ({
27
+ setQuickActions: create.reducer(
28
+ (state, action: PayloadAction<QuickAction[]>) => {
29
+ state.quickActions = action.payload;
30
+ },
31
+ ),
32
+ setSidebarActions: create.reducer(
33
+ (state, action: PayloadAction<QuickAction[]>) => {
34
+ state.sidebarActions = action.payload;
35
+ },
36
+ ),
37
+ }),
38
+ // You can define your selectors here. These selectors receive the slice
39
+ // state as their first argument.
40
+ selectors: {
41
+ selectQuickActions: (navbar) => navbar.quickActions,
42
+ selectSidebarActions: (navbar) => navbar.sidebarActions,
43
+ },
44
+ });
45
+
46
+ // Action creators are generated for each case reducer function.
47
+ export const { setQuickActions, setSidebarActions } = navbarSlice.actions;
48
+
49
+ // Selectors returned by `slice.selectors` take the root state as their first argument.
50
+ export const { selectQuickActions, selectSidebarActions } =
51
+ navbarSlice.selectors;
@@ -0,0 +1,11 @@
1
+ import { StudentInfo } from "../activityData/activityDataSlice";
2
+
3
+ export function studentNameFromInfo(studentInfo: StudentInfo | null) {
4
+ if (!studentInfo || (!studentInfo.firstName && !studentInfo.lastName)) {
5
+ return null;
6
+ }
7
+ if (studentInfo.class && studentInfo.class !== "enseignants") {
8
+ return `${studentInfo.firstName} ${studentInfo.lastName} — ${studentInfo.class}`;
9
+ }
10
+ return `${studentInfo.firstName} ${studentInfo.lastName}`;
11
+ }
@@ -0,0 +1,162 @@
1
+ .navbar {
2
+ --navbar-text-color: rgba(255, 255, 255, 0.95);
3
+ background-color: var(--surface-800);
4
+ display: flex;
5
+ align-items: center;
6
+ color: var(--navbar-text-color);
7
+ overflow: hidden;
8
+ color-scheme: dark;
9
+ :global(.dark-theme) & {
10
+ --navbar-text-color: var(--text-color);
11
+ background-color: var(--surface-50);
12
+ }
13
+ & :global(.p-button-outlined),
14
+ & :global(.p-button-text) {
15
+ color: var(--navbar-text-color);
16
+ }
17
+ & :global(.p-button-outlined) {
18
+ border-color: var(--navbar-text-color);
19
+ }
20
+ :global(.light-theme) & :global(.p-button.p-button-secondary:enabled:focus) {
21
+ box-shadow:
22
+ 0 0 0 2px #1c2127,
23
+ 0 0 0 4px rgba(255, 255, 255, 0.7),
24
+ 0 1px 2px 0 rgba(0, 0, 0, 0);
25
+ }
26
+ :global(.light-theme) & :global(.p-button.p-button-warning:enabled:focus) {
27
+ box-shadow:
28
+ 0 0 0 2px #1c2127,
29
+ 0 0 0 4px rgba(249, 115, 22, 0.7),
30
+ 0 1px 2px 0 rgba(0, 0, 0, 0);
31
+ }
32
+ }
33
+
34
+ .reviewNavbar {
35
+ color-scheme: dark;
36
+ background-color: var(--surface-500);
37
+ --review-navbar-text-color: rgba(255, 255, 255, 0.95);
38
+ color: var(--review-navbar-text-color);
39
+ padding: 4px 16px;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ & :global(.p-button-secondary) {
44
+ border-color: var(--review-navbar-text-color);
45
+ color: var(--review-navbar-text-color);
46
+ }
47
+ & :global(.p-dropdown) {
48
+ border-color: var(--review-navbar-text-color);
49
+ border-left-width: 0;
50
+ border-right-width: 0;
51
+ }
52
+ :global(.dark-theme) & {
53
+ background-color: var(--surface-100);
54
+ }
55
+ }
56
+
57
+ .navbarContainer {
58
+ height: 60px;
59
+ flex-grow: 1;
60
+ display: flex;
61
+ justify-content: space-between;
62
+ align-items: center;
63
+ gap: 16px;
64
+ overflow: hidden;
65
+ & > * {
66
+ flex-shrink: 0;
67
+ }
68
+ }
69
+
70
+ .navbarLogo {
71
+ font-family: Ubuntu, Tahoma, "Helvetica Neue", Helvetica, Arial, sans-serif;
72
+ font-weight: 700;
73
+ font-size: 24px;
74
+ letter-spacing: 2px;
75
+ height: 100%;
76
+ & > * {
77
+ height: 100%;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ padding: 0 16px;
82
+ color: var(--navbar-text-color);
83
+ text-decoration: none;
84
+ &:hover {
85
+ background-color: rgba(255, 255, 255, 0.1);
86
+ }
87
+ }
88
+ }
89
+
90
+ .capytaleMenu {
91
+ display: flex;
92
+ gap: 8px;
93
+ }
94
+
95
+ .activityInfo {
96
+ flex-shrink: 1;
97
+ flex-grow: 1;
98
+ display: flex;
99
+ justify-content: center;
100
+ align-items: center;
101
+ overflow: hidden;
102
+ gap: 12px;
103
+ }
104
+
105
+ .activityInfoText {
106
+ display: flex;
107
+ flex-direction: column;
108
+ justify-content: center;
109
+ align-items: center;
110
+ overflow: hidden;
111
+ & > * {
112
+ overflow: hidden;
113
+ text-overflow: ellipsis;
114
+ white-space: nowrap;
115
+ max-width: 100%;
116
+ }
117
+ }
118
+
119
+ .activityInfoTitle {
120
+ font-size: 20px;
121
+ font-weight: 500;
122
+ }
123
+
124
+ .activityLogo {
125
+ width: 35px;
126
+ height: 37px;
127
+ border-radius: 6px;
128
+ flex-shrink: 0;
129
+ @media only screen and (max-width: 992px) {
130
+ display: none;
131
+ }
132
+ }
133
+
134
+ .activityMenu {
135
+ padding-right: 16px;
136
+ display: flex;
137
+ gap: 8px;
138
+ justify-content: center;
139
+ align-items: center;
140
+ }
141
+
142
+ .sidebarFieldset {
143
+ margin-bottom: 8px;
144
+ }
145
+
146
+ .sidebarFieldsetButtons :global(.p-fieldset-content) {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 6px;
150
+ }
151
+
152
+ .sidebarRadioButtons {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 4px;
156
+ }
157
+
158
+ .sidebarRadioGroup {
159
+ display: flex;
160
+ gap: 8px;
161
+ align-items: center;
162
+ }
@@ -0,0 +1,65 @@
1
+ import { useCapytaleRichTextEditor } from "@capytale/capytale-rich-text-editor";
2
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
3
+ import {
4
+ selectAnswerSheetContent,
5
+ selectMode,
6
+ setCanSaveAnswerSheet,
7
+ setLexicalAnswerSheetState,
8
+ } from "../activityData/activityDataSlice";
9
+ import { forwardRef, useImperativeHandle, useRef } from "react";
10
+ import { Toast } from "primereact/toast";
11
+ import settings from "../../settings";
12
+
13
+ const AnswerSheetEditor: React.FC = forwardRef((_props, ref) => {
14
+ const [Editor, getState, _canSave] = useCapytaleRichTextEditor();
15
+ const dispatch = useAppDispatch();
16
+ const toast = useRef<Toast>(null);
17
+ const mode = useAppSelector(selectMode);
18
+ const isEditable =
19
+ mode === "create" || mode === "assignment" || mode === "review";
20
+ const initialEditorState = useAppSelector(selectAnswerSheetContent);
21
+
22
+ useImperativeHandle(ref, () => {
23
+ return {
24
+ save: async () => {
25
+ const state = await getState();
26
+ dispatch(setLexicalAnswerSheetState(state.json));
27
+ },
28
+ };
29
+ });
30
+
31
+ return (
32
+ <>
33
+ <Toast position="bottom-right" ref={toast} />
34
+ <Editor
35
+ placeholderText="Écrivez le contenu initial de la fiche réponse ici..."
36
+ initialEditorState={initialEditorState}
37
+ jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
38
+ onChange={(editorState) => {
39
+ dispatch(setLexicalAnswerSheetState(editorState));
40
+ }}
41
+ onJsonSizeLimitExceeded={() => {
42
+ dispatch(setCanSaveAnswerSheet(false));
43
+ toast.current!.show({
44
+ summary: "Erreur",
45
+ detail: `Le contenu de la fiche réponse est trop volumineux. Veuillez le réduire avant de l'enregistrer.\nPeut-être avez-vous inséré une image trop volumineuse ?`,
46
+ severity: "error",
47
+ life: 10000,
48
+ });
49
+ }}
50
+ onJsonSizeLimitMet={() => {
51
+ dispatch(setCanSaveAnswerSheet(true));
52
+ toast.current!.show({
53
+ summary: "Succès",
54
+ detail: `Le contenu de la fiche réponse ne dépasse plus la taille limite.`,
55
+ severity: "success",
56
+ life: 4000,
57
+ });
58
+ }}
59
+ isEditable={isEditable}
60
+ />
61
+ </>
62
+ );
63
+ });
64
+
65
+ export default AnswerSheetEditor;
@@ -0,0 +1,82 @@
1
+ import { useCapytaleRichTextEditor } from "@capytale/capytale-rich-text-editor";
2
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
3
+ import {
4
+ selectInstructions,
5
+ selectMode,
6
+ setCanSaveInstructions,
7
+ setLexicalInstructionsState,
8
+ } from "../activityData/activityDataSlice";
9
+ import {
10
+ forwardRef,
11
+ useImperativeHandle,
12
+ useMemo,
13
+ useRef,
14
+ } from "react";
15
+ import { Toast } from "primereact/toast";
16
+ import settings from "../../settings";
17
+
18
+ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
19
+ const [Editor, getState, _canSave] = useCapytaleRichTextEditor();
20
+ const dispatch = useAppDispatch();
21
+ const toast = useRef<Toast>(null);
22
+ const mode = useAppSelector(selectMode);
23
+ const isEditable = mode === "create";
24
+ const initialInstructions = useAppSelector(selectInstructions);
25
+ const htmlInitialContent = useMemo(() => {
26
+ return initialInstructions?.format === "lexical"
27
+ ? undefined
28
+ : initialInstructions?.value === null
29
+ ? undefined
30
+ : (initialInstructions?.value as string) || ""; // HTML content in value
31
+ }, [initialInstructions]);
32
+ const initialEditorState = useMemo(() => {
33
+ return initialInstructions?.format === "lexical"
34
+ ? initialInstructions?.value
35
+ : undefined;
36
+ }, [initialInstructions]);
37
+
38
+ useImperativeHandle(ref, () => {
39
+ return {
40
+ save: async () => {
41
+ const state = await getState();
42
+ dispatch(setLexicalInstructionsState(state.json));
43
+ },
44
+ };
45
+ });
46
+
47
+ return (
48
+ <>
49
+ <Toast position="bottom-right" ref={toast} />
50
+ <Editor
51
+ placeholderText="Écrivez la consigne ici..."
52
+ htmlInitialContent={htmlInitialContent}
53
+ initialEditorState={initialEditorState}
54
+ jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
55
+ onChange={(editorState) => {
56
+ dispatch(setLexicalInstructionsState(editorState));
57
+ }}
58
+ onJsonSizeLimitExceeded={() => {
59
+ dispatch(setCanSaveInstructions(false));
60
+ toast.current!.show({
61
+ summary: "Erreur",
62
+ detail: `Le contenu de la consigne est trop volumineux. Veuillez le réduire avant de l'enregistrer.\nPeut-être avez-vous inséré une image trop volumineuse ?`,
63
+ severity: "error",
64
+ life: 10000,
65
+ });
66
+ }}
67
+ onJsonSizeLimitMet={() => {
68
+ dispatch(setCanSaveInstructions(true));
69
+ toast.current!.show({
70
+ summary: "Succès",
71
+ detail: `Le contenu de la consigne ne dépasse plus la taille limite.`,
72
+ severity: "success",
73
+ life: 4000,
74
+ });
75
+ }}
76
+ isEditable={isEditable}
77
+ />
78
+ </>
79
+ );
80
+ });
81
+
82
+ export default InstructionsEditor;