@capytale/meta-player 0.0.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -24,7 +24,6 @@
24
24
  "react-dom": "^18.2.0",
25
25
  "react-html-props": "^2.0.9",
26
26
  "react-redux": "^9.1.0",
27
- "react-reverse-portal": "^2.1.2",
28
27
  "screenfull": "^6.0.2"
29
28
  },
30
29
  "devDependencies": {
package/src/App.tsx CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  selectOrientation,
14
14
  toggleIsPedagoVisible,
15
15
  } from "./features/layout/layoutSlice";
16
- import { FC, PropsWithChildren, useMemo } from "react";
16
+ import { FC, PropsWithChildren } from "react";
17
17
  import { Tooltip } from "primereact/tooltip";
18
18
  import {
19
19
  selectHasGradingOrComments,
@@ -23,8 +23,6 @@ import {
23
23
  import settings from "./settings";
24
24
  import ReviewNavbar from "./features/navbar/ReviewNavbar";
25
25
 
26
- import * as portals from "react-reverse-portal";
27
-
28
26
  type AppProps = PropsWithChildren<{}>;
29
27
 
30
28
  const App: FC<AppProps> = (props) => {
@@ -36,8 +34,7 @@ const App: FC<AppProps> = (props) => {
36
34
  const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
37
35
  const hasPedago = hasInstructions || hasGradingOrComments;
38
36
  const dispatch = useAppDispatch();
39
-
40
- const portalNode = useMemo(() => portals.createHtmlPortalNode(), []);
37
+ const showPedago = hasPedago && isPedagoVisible;
41
38
 
42
39
  return (
43
40
  <div
@@ -47,78 +44,69 @@ const App: FC<AppProps> = (props) => {
47
44
  isHorizontal ? "layout-horizontal" : "layout-vertical",
48
45
  )}
49
46
  >
50
- <portals.InPortal node={portalNode}>
51
- <div id="meta-player-content">{props.children}</div>
52
- </portals.InPortal>
53
47
  <div>
54
48
  <Navbar />
55
49
  {mode === "review" && <ReviewNavbar />}
56
50
  </div>
57
- {hasPedago && isPedagoVisible && (
58
- <Splitter
59
- className={styles.appPedagoSplitter}
60
- layout={isHorizontal ? "vertical" : "horizontal"}
51
+ <div className={styles.pedagoContainer} data-show-pedago={showPedago}>
52
+ <div
53
+ className={classNames(
54
+ styles.hiddenPedago,
55
+ hasPedago ? null : styles.noPedago,
56
+ )}
61
57
  >
62
- <SplitterPanel minSize={15} size={30} className={styles.pedagoPanel}>
63
- <Pedago key="pedago" />
64
- </SplitterPanel>
65
- <SplitterPanel minSize={40} size={70}>
66
- <portals.OutPortal node={portalNode} />
67
- </SplitterPanel>
68
- </Splitter>
69
- )}
70
- {(!hasPedago || !isPedagoVisible) && (
71
- <div className={styles.hiddenPedagoContainer}>
72
58
  <div
73
59
  className={classNames(
74
- styles.hiddenPedago,
75
- hasPedago ? null : styles.noPedago,
60
+ styles.hiddenPedagoButton,
61
+ hasPedago ? "p-ripple" : null,
76
62
  )}
63
+ onClick={
64
+ hasPedago ? () => dispatch(toggleIsPedagoVisible()) : undefined
65
+ }
66
+ data-pr-tooltip={
67
+ hasPedago
68
+ ? "Afficher les consignes"
69
+ : "Pas de consignes ni de note"
70
+ }
71
+ aria-label={
72
+ hasPedago
73
+ ? "Afficher les consignes"
74
+ : "Pas de consignes ni de note"
75
+ }
76
+ role={hasPedago ? "button" : "note"}
77
77
  >
78
- <div
78
+ <i
79
79
  className={classNames(
80
- styles.hiddenPedagoButton,
81
- hasPedago ? "p-ripple" : null,
82
- )}
83
- onClick={
84
- hasPedago ? () => dispatch(toggleIsPedagoVisible()) : undefined
85
- }
86
- data-pr-tooltip={
87
- hasPedago
88
- ? "Afficher les consignes"
89
- : "Pas de consignes ni de note"
90
- }
91
- aria-label={
80
+ "pi",
92
81
  hasPedago
93
- ? "Afficher les consignes"
94
- : "Pas de consignes ni de note"
95
- }
96
- role={hasPedago ? "button" : "note"}
97
- >
98
- <i
99
- className={classNames(
100
- "pi",
101
- hasPedago
102
- ? isHorizontal
103
- ? "pi-angle-double-down"
104
- : "pi-angle-double-right"
105
- : "pi-minus-circle",
106
- )}
107
- />
108
- <Ripple />
109
- </div>
110
- <Tooltip
111
- target={"." + styles.hiddenPedagoButton}
112
- showDelay={settings.TOOLTIP_SHOW_DELAY}
113
- position={isHorizontal ? "bottom" : "right"}
114
- mouseTrack={!isHorizontal}
82
+ ? isHorizontal
83
+ ? "pi-angle-double-down"
84
+ : "pi-angle-double-right"
85
+ : "pi-minus-circle",
86
+ )}
115
87
  />
88
+ <Ripple />
116
89
  </div>
117
- <div className={styles.hiddenPedagoContent}>
118
- <portals.OutPortal node={portalNode} />
119
- </div>
90
+ <Tooltip
91
+ target={"." + styles.hiddenPedagoButton}
92
+ showDelay={settings.TOOLTIP_SHOW_DELAY}
93
+ position={isHorizontal ? "bottom" : "right"}
94
+ mouseTrack={!isHorizontal}
95
+ />
120
96
  </div>
121
- )}
97
+ <Splitter
98
+ className={styles.appPedagoSplitter}
99
+ layout={isHorizontal ? "vertical" : "horizontal"}
100
+ >
101
+ <SplitterPanel minSize={15} size={30} className={styles.pedagoPanel}>
102
+ <Pedago key="pedago" />
103
+ </SplitterPanel>
104
+ <SplitterPanel minSize={40} size={70}>
105
+ <div id="meta-player-content-cover"></div>
106
+ <div id="meta-player-content">{props.children}</div>
107
+ </SplitterPanel>
108
+ </Splitter>
109
+ </div>
122
110
  </div>
123
111
  );
124
112
  };
@@ -1,4 +1,4 @@
1
- import React, { PropsWithChildren, useMemo } from "react";
1
+ import React, { PropsWithChildren } from "react";
2
2
 
3
3
  import { Provider } from "react-redux";
4
4
 
@@ -12,30 +12,19 @@ import ThemeSwitcher from "./features/theming/ThemeSwitcher";
12
12
  import { ActivityJSProvider } from "./features/activityJS/ActivityJSProvider";
13
13
  import { LoadOptions } from "./features/activityJS/hooks";
14
14
  import { ErrorBoundary } from "./utils/ErrorBoundary";
15
- import {
16
- MetaPlayerOptions,
17
- defaultMetaPlayerOptions,
18
- } from "./features/activityData/metaPlayerOptions";
19
- import OptionSetter from "./features/activityData/OptionSetter";
20
15
  import Saver from "./features/activityJS/Saver";
21
16
 
22
17
  type MetaPlayerProps = PropsWithChildren<{
23
18
  activityJSOptions?: LoadOptions;
24
- options?: Partial<MetaPlayerOptions>;
25
19
  }>;
26
20
 
27
21
  const MetaPlayer: React.FC<MetaPlayerProps> = (props) => {
28
22
  const primeSettings: Partial<APIOptions> = {
29
23
  ripple: true,
30
24
  };
31
- const options = useMemo(
32
- () => ({ ...defaultMetaPlayerOptions, ...props.options }),
33
- [props.options],
34
- );
35
25
  return (
36
26
  <PrimeReactProvider value={primeSettings}>
37
27
  <Provider store={store}>
38
- <OptionSetter options={options} />
39
28
  <ThemeSwitcher />
40
29
  <ErrorBoundary fallback={<div>Une erreur est survenue</div>}>
41
30
  <ActivityJSProvider options={props.activityJSOptions}>
package/src/app/store.ts CHANGED
@@ -6,6 +6,7 @@ import { layoutSlice } from "../features/layout/layoutSlice";
6
6
  import { activityDataSlice } from "../features/activityData/activityDataSlice";
7
7
  import { navbarSlice } from "../features/navbar/navbarSlice";
8
8
  import { saverSlice } from "../features/activityJS/saverSlice";
9
+ import { activitySettingsSlice } from "../features/activitySettings/activitySettingsSlice";
9
10
 
10
11
  // `combineSlices` automatically combines the reducers using
11
12
  // their `reducerPath`s, therefore we no longer need to call `combineReducers`.
@@ -15,6 +16,7 @@ const rootReducer = combineSlices(
15
16
  layoutSlice,
16
17
  navbarSlice,
17
18
  saverSlice,
19
+ activitySettingsSlice,
18
20
  );
19
21
  // Infer the `RootState` type from the root reducer
20
22
  export type RootState = ReturnType<typeof rootReducer>;
@@ -9,6 +9,14 @@
9
9
  border: none;
10
10
  border-radius: 0;
11
11
  overflow-y: hidden;
12
+ .pedagoContainer[data-show-pedago="false"] & {
13
+ & > :first-child {
14
+ display: none;
15
+ }
16
+ & > :nth-child(2) {
17
+ display: none;
18
+ }
19
+ }
12
20
  }
13
21
 
14
22
  .pedagoPanel {
@@ -17,7 +25,8 @@
17
25
  overflow-y: hidden;
18
26
  }
19
27
 
20
- .hiddenPedagoContainer {
28
+ .pedagoContainer {
29
+ overflow-y: hidden;
21
30
  display: grid;
22
31
  :global(.layout-horizontal) & {
23
32
  grid-template-columns: 1fr;
@@ -27,10 +36,16 @@
27
36
  grid-template-rows: 1fr;
28
37
  grid-template-columns: 1.2rem 1fr;
29
38
  }
39
+ &[data-show-pedago="true"] {
40
+ display: block;
41
+ }
30
42
  }
31
43
 
32
44
  .hiddenPedago {
33
45
  background-color: var(--surface-200);
46
+ .pedagoContainer[data-show-pedago="true"] & {
47
+ display: none;
48
+ }
34
49
  }
35
50
 
36
51
  .hiddenPedagoButton {
@@ -50,7 +65,3 @@
50
65
  background-color: rgba(128, 128, 128, 0.2);
51
66
  }
52
67
  }
53
-
54
- .hiddenPedagoContent {
55
- background-color: var(--surface-a);
56
- }
package/src/demo.tsx CHANGED
@@ -6,6 +6,7 @@ import { ActivityQuickActionsSetter } from "./features/navbar/ActivityQuickActio
6
6
  import { ActivitySidebarActionsSetter } from "./features/navbar/ActivitySidebarActions";
7
7
  // import { useActivityJS } from "./features/activityJS/ActivityJSProvider";
8
8
  import BeforeSaveAction from "./features/activityJS/BeforeSaveAction";
9
+ import MetaPlayerOptionsSetter from "./features/activityData/MetaPlayerOptionsSetter";
9
10
 
10
11
  const DemoActivity: FC = () => {
11
12
  // const activityJs = useActivityJS();
@@ -62,14 +63,15 @@ if (container) {
62
63
 
63
64
  root.render(
64
65
  <React.StrictMode>
65
- <MetaPlayer
66
- options={{
67
- hasInstructions: true,
68
- pedagoLayout: "default-horizontal",
69
- supportsLightTheme: true,
70
- supportsDarkTheme: true,
71
- }}
72
- >
66
+ <MetaPlayer>
67
+ <MetaPlayerOptionsSetter
68
+ options={{
69
+ hasInstructions: true,
70
+ pedagoLayout: "default-horizontal",
71
+ supportsLightTheme: true,
72
+ supportsDarkTheme: true,
73
+ }}
74
+ />
73
75
  <DemoActivity />
74
76
  </MetaPlayer>
75
77
  </React.StrictMode>,
@@ -0,0 +1,5 @@
1
+ import { Toast } from "primereact/toast";
2
+ import type { ToastMessage } from "primereact/toast";
3
+
4
+ export { Toast };
5
+ export type { ToastMessage };
@@ -14,10 +14,4 @@ const IsDirtySetter: FC<IsDirtySetterProps> = (props) => {
14
14
  return null;
15
15
  };
16
16
 
17
- const notifyIsDirty = (isDirty: boolean = true) => {
18
- const dispatch = useAppDispatch();
19
- dispatch(setIsPlayerDirty(isDirty));
20
- };
21
-
22
17
  export default IsDirtySetter;
23
- export { notifyIsDirty };
@@ -0,0 +1,41 @@
1
+ import { FC, useEffect } from "react";
2
+ import { useAppDispatch } from "../../app/hooks";
3
+ import { setPlayerSettings } from "./activityDataSlice";
4
+ import { setLayout } from "../layout/layoutSlice";
5
+ import { setAutoSwitch, switchToTheme } from "../theming/themingSlice";
6
+ import {
7
+ defaultMetaPlayerOptions,
8
+ MetaPlayerOptions,
9
+ } from "./metaPlayerOptions";
10
+
11
+ type MetaPlayerOptionsSetterProps = {
12
+ options: Partial<MetaPlayerOptions>;
13
+ };
14
+
15
+ const MetaPlayerOptionsSetter: FC<MetaPlayerOptionsSetterProps> = (props) => {
16
+ const dispatch = useAppDispatch();
17
+ useEffect(() => {
18
+ const options = { ...defaultMetaPlayerOptions, ...props.options };
19
+ if (!options.supportsLightTheme && !options.supportsDarkTheme) {
20
+ throw new Error("At least one theme must be supported");
21
+ }
22
+ dispatch(setPlayerSettings(options));
23
+ dispatch(
24
+ setLayout(
25
+ options.pedagoLayout === "horizontal" ||
26
+ options.pedagoLayout === "default-horizontal"
27
+ ? "horizontal"
28
+ : "vertical",
29
+ ),
30
+ );
31
+ if (options.supportsDarkTheme && options.supportsLightTheme) {
32
+ dispatch(setAutoSwitch(true));
33
+ } else {
34
+ dispatch(setAutoSwitch(false));
35
+ dispatch(switchToTheme(options.supportsLightTheme ? "light" : "dark"));
36
+ }
37
+ }, [dispatch, props.options]);
38
+ return null;
39
+ };
40
+
41
+ export default MetaPlayerOptionsSetter;
@@ -0,0 +1,9 @@
1
+ import { useAppDispatch } from "../../app/hooks";
2
+ import { setIsPlayerDirty } from "./activityDataSlice";
3
+
4
+ const useNotifyIsDirty = (isDirty: boolean = true) => {
5
+ const dispatch = useAppDispatch();
6
+ return () => dispatch(setIsPlayerDirty(isDirty));
7
+ };
8
+
9
+ export { useNotifyIsDirty };
@@ -89,7 +89,6 @@ const Saver: FC<{}> = () => {
89
89
  activityJs.activitySession.mode === "assignment" ||
90
90
  activityJs.activitySession.mode === "review"
91
91
  ) {
92
- console.log("Workflow:", workflow);
93
92
  ab.assignmentNode!.workflow = workflow!;
94
93
  }
95
94
  try {
@@ -0,0 +1,26 @@
1
+ import { FC, useEffect, useRef } from "react";
2
+ import { useAppDispatch } from "../../app/hooks";
3
+ import { ActivitySettings } from "./types";
4
+ import { setSettings } from "./activitySettingsSlice";
5
+ import { deepEqual } from "../../utils/equality";
6
+
7
+ type ActivitySettingsSetterProps = {
8
+ settings: ActivitySettings;
9
+ };
10
+
11
+ const ActivitySettingsSetter: FC<ActivitySettingsSetterProps> = ({
12
+ settings,
13
+ }) => {
14
+ const oldSettings = useRef<ActivitySettings | null>(null);
15
+ const dispatch = useAppDispatch();
16
+ useEffect(() => {
17
+ if (deepEqual(oldSettings.current, settings)) {
18
+ return;
19
+ }
20
+ oldSettings.current = settings;
21
+ dispatch(setSettings(settings));
22
+ }, [dispatch, settings]);
23
+ return null;
24
+ };
25
+
26
+ export default ActivitySettingsSetter;
@@ -0,0 +1,43 @@
1
+ import type { PayloadAction } from "@reduxjs/toolkit";
2
+ import { createAppSlice } from "../../app/createAppSlice";
3
+ import { ActivitySettings } from "./types";
4
+
5
+ // If you are not using async thunks you can use the standalone `createSlice`.
6
+ export const activitySettingsSlice = createAppSlice({
7
+ name: "activitySettings",
8
+ // `createSlice` will infer the state type from the `initialState` argument
9
+ initialState: {
10
+ settings: undefined as ActivitySettings | undefined,
11
+ },
12
+ // The `reducers` field lets us define reducers and generate associated actions
13
+ reducers: (create) => ({
14
+ setSettings: create.reducer(
15
+ (state, action: PayloadAction<ActivitySettings | undefined>) => {
16
+ state.settings = action.payload;
17
+ },
18
+ ),
19
+ updateSettings: create.reducer(
20
+ (
21
+ state,
22
+ action: PayloadAction<
23
+ (
24
+ oldSettings: ActivitySettings | undefined,
25
+ ) => ActivitySettings | undefined
26
+ >,
27
+ ) => {
28
+ state.settings = action.payload(state.settings);
29
+ },
30
+ ),
31
+ }),
32
+ // You can define your selectors here. These selectors receive the slice
33
+ // state as their first argument.
34
+ selectors: {
35
+ selectSettings: (data) => data.settings,
36
+ },
37
+ });
38
+
39
+ // Action creators are generated for each case reducer function.
40
+ export const { setSettings, updateSettings } = activitySettingsSlice.actions;
41
+
42
+ // Selectors returned by `slice.selectors` take the root state as their first argument.
43
+ export const { selectSettings } = activitySettingsSlice.selectors;
@@ -0,0 +1,6 @@
1
+ import { useAppSelector } from "../../app/hooks";
2
+ import { selectSettings } from "./activitySettingsSlice";
3
+
4
+ export const useActivitySettings = () => {
5
+ return useAppSelector(selectSettings);
6
+ };
@@ -0,0 +1,32 @@
1
+ import { Fieldset } from "primereact/fieldset";
2
+ import { useActivitySettings } from "./hooks";
3
+ import {
4
+ ActivitySettingsFormDisplay,
5
+ ActivitySettingsOptionsDisplay,
6
+ } from "./ui";
7
+
8
+ type ActivitySettingsDisplayProps = {};
9
+
10
+ export function ActivitySettingsDisplay({}: ActivitySettingsDisplayProps) {
11
+ const settings = useActivitySettings();
12
+ if (!settings) return null;
13
+ return (
14
+ <div>
15
+ {Object.keys(settings).map((id) => {
16
+ const section = settings[id];
17
+ return (
18
+ <Fieldset key={id} legend={section.title} className="sidebarFieldset">
19
+ {section.type === "form" ? (
20
+ <ActivitySettingsFormDisplay form={section} sectionId={id} />
21
+ ) : (
22
+ <ActivitySettingsOptionsDisplay
23
+ options={section}
24
+ sectionId={id}
25
+ />
26
+ )}
27
+ </Fieldset>
28
+ );
29
+ })}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,4 @@
1
+ .formLabel {
2
+ }
3
+ .formGroup {
4
+ }
@@ -0,0 +1,88 @@
1
+ export type ActivitySettingsSelect = {
2
+ type: "select";
3
+ options: {
4
+ label: string;
5
+ name: string;
6
+ }[];
7
+ selectedOptionName: string;
8
+ };
9
+
10
+ export type ActivitySettingsRange = {
11
+ type: "range";
12
+ min: number;
13
+ max: number;
14
+ step?: number;
15
+ value: number;
16
+ };
17
+
18
+ export type ActivitySettingsTextArea = {
19
+ type: "textarea";
20
+ value: string;
21
+ };
22
+
23
+ export type ActivitySettingsOption = {
24
+ type: "checkbox" | "switch";
25
+ value: boolean;
26
+ };
27
+
28
+ export type ActivitySettingsInput = {
29
+ type: "input";
30
+ inputType:
31
+ | "color"
32
+ | "date"
33
+ | "datetime-local"
34
+ | "email"
35
+ | "month"
36
+ | "number"
37
+ | "password"
38
+ | "search"
39
+ | "tel"
40
+ | "text"
41
+ | "time"
42
+ | "url"
43
+ | "week";
44
+ value: string | number;
45
+ };
46
+
47
+ export type ActivitySettingsFormSection = {
48
+ type: "form";
49
+ fields: {
50
+ [name: string]: (
51
+ | ActivitySettingsRange
52
+ | ActivitySettingsInput
53
+ | ActivitySettingsTextArea
54
+ | ActivitySettingsOption
55
+ | ActivitySettingsSelect
56
+ ) & {
57
+ label: string;
58
+ };
59
+ };
60
+ };
61
+
62
+ export type ActivitySettingsMultipleOptionsSection = {
63
+ type: "checkboxes" | "switches";
64
+ options: {
65
+ label: string;
66
+ name: string;
67
+ }[];
68
+ selectedOptionNames: string[];
69
+ };
70
+
71
+ export type ActivitySettingsRadioOptionsSection = {
72
+ type: "radio";
73
+ options: {
74
+ label: string;
75
+ name: string;
76
+ }[];
77
+ selectedOptionName: string | null;
78
+ };
79
+
80
+ export type ActivitySettingsSection = (
81
+ | ActivitySettingsMultipleOptionsSection
82
+ | ActivitySettingsRadioOptionsSection
83
+ | ActivitySettingsFormSection
84
+ ) & {
85
+ title: string;
86
+ };
87
+
88
+ export type ActivitySettings = { [id: string]: ActivitySettingsSection };
@@ -0,0 +1,393 @@
1
+ import type {
2
+ ActivitySettings,
3
+ ActivitySettingsFormSection,
4
+ ActivitySettingsMultipleOptionsSection,
5
+ ActivitySettingsRadioOptionsSection,
6
+ } from "./types";
7
+
8
+ import styles from "./style.module.scss";
9
+ import { Slider } from "primereact/slider";
10
+ import { InputNumber } from "primereact/inputnumber";
11
+ import { InputText } from "primereact/inputtext";
12
+ import { ColorPicker } from "primereact/colorpicker";
13
+ import { InputTextarea } from "primereact/inputtextarea";
14
+ import { Checkbox } from "primereact/checkbox";
15
+ import { InputSwitch } from "primereact/inputswitch";
16
+ import { Dropdown } from "primereact/dropdown";
17
+ import React from "react";
18
+ import { RadioButton } from "primereact/radiobutton";
19
+ import { useAppDispatch } from "../../app/hooks";
20
+ import { updateSettings } from "./activitySettingsSlice";
21
+
22
+ type ActivitySettingsFormDisplayProps = {
23
+ form: ActivitySettingsFormSection;
24
+ sectionId: string;
25
+ };
26
+
27
+ export function ActivitySettingsFormDisplay({
28
+ form,
29
+ sectionId,
30
+ }: ActivitySettingsFormDisplayProps) {
31
+ const dispatch = useAppDispatch();
32
+ const selectOnChange = (fieldName: string, value: string) => {
33
+ dispatch(
34
+ updateSettings((oldSettings: ActivitySettings | undefined) => {
35
+ if (!oldSettings) {
36
+ return undefined;
37
+ }
38
+ const oldSection = oldSettings[
39
+ sectionId
40
+ ] as ActivitySettingsFormSection;
41
+ const newSettings = {
42
+ ...oldSettings,
43
+ [sectionId]: {
44
+ ...oldSection,
45
+ type: "form",
46
+ fields: {
47
+ ...oldSection.fields,
48
+ [fieldName]: {
49
+ ...oldSection.fields[fieldName],
50
+ selectedOptionName: value,
51
+ },
52
+ },
53
+ },
54
+ };
55
+ return newSettings as ActivitySettings;
56
+ }),
57
+ );
58
+ };
59
+ const onChange = (fieldName: string, value: string | number | boolean) => {
60
+ dispatch(
61
+ updateSettings((oldSettings: ActivitySettings | undefined) => {
62
+ if (!oldSettings) {
63
+ return undefined;
64
+ }
65
+ const oldSection = oldSettings[
66
+ sectionId
67
+ ] as ActivitySettingsFormSection;
68
+ const newSettings = {
69
+ ...oldSettings,
70
+ [sectionId]: {
71
+ ...oldSection,
72
+ type: "form",
73
+ fields: {
74
+ ...oldSection.fields,
75
+ [fieldName]: {
76
+ ...oldSection.fields[fieldName],
77
+ value,
78
+ },
79
+ },
80
+ },
81
+ };
82
+ return newSettings as ActivitySettings;
83
+ }),
84
+ );
85
+ };
86
+ return (
87
+ <div className="mb-3">
88
+ {Object.keys(form.fields).map((name, _) => {
89
+ const field = form.fields[name];
90
+ switch (field.type) {
91
+ case "range":
92
+ return (
93
+ <div className={styles.formGroup}>
94
+ <label
95
+ className={styles.formLabel}
96
+ htmlFor={sectionId + "-" + name}
97
+ >
98
+ Range
99
+ </label>
100
+ <InputNumber
101
+ min={field.min}
102
+ max={field.max}
103
+ step={field.step}
104
+ value={field.value}
105
+ onChange={(e) => onChange(name, e.value as number)}
106
+ />
107
+ <Slider
108
+ id={sectionId + "-" + name}
109
+ min={field.min}
110
+ max={field.max}
111
+ step={field.step}
112
+ value={field.value}
113
+ onChange={(e) => onChange(name, e.value as number)}
114
+ />
115
+ </div>
116
+ );
117
+ case "input":
118
+ return (
119
+ <div className={styles.formGroup}>
120
+ <label
121
+ className={styles.formLabel}
122
+ htmlFor={sectionId + "-" + name}
123
+ >
124
+ {field.label}
125
+ </label>
126
+ {field.inputType === "text" && (
127
+ <InputText
128
+ id={sectionId + "-" + name}
129
+ value={field.value as string}
130
+ onChange={(e) => onChange(name, e.target.value)}
131
+ />
132
+ )}
133
+ {field.inputType === "number" && (
134
+ <InputNumber
135
+ inputId={sectionId + "-" + name}
136
+ value={field.value as number}
137
+ onChange={(e) => e.value != null && onChange(name, e.value)}
138
+ />
139
+ )}
140
+ {field.inputType === "color" && (
141
+ <ColorPicker
142
+ inputId={sectionId + "-" + name}
143
+ value={field.value as string}
144
+ onChange={(e) =>
145
+ e.value && onChange(name, e.value as string)
146
+ }
147
+ />
148
+ )}
149
+ {field.inputType !== "color" &&
150
+ field.inputType !== "number" &&
151
+ field.inputType !== "text" && (
152
+ <p>Type d'input {field.inputType} pas implémenté.</p>
153
+ )}
154
+ </div>
155
+ );
156
+ case "textarea":
157
+ return (
158
+ <div className={styles.formGroup}>
159
+ <label
160
+ className={styles.formLabel}
161
+ htmlFor={sectionId + "-" + name}
162
+ >
163
+ {field.label}
164
+ </label>
165
+ <InputTextarea
166
+ id={sectionId + "-" + name}
167
+ value={field.value}
168
+ onChange={(e) => onChange(name, e.target.value)}
169
+ />
170
+ </div>
171
+ );
172
+ case "checkbox":
173
+ return (
174
+ <CheckboxGroup
175
+ inputId={sectionId + "-" + name}
176
+ checked={field.value}
177
+ onChange={(value) => onChange(name, value)}
178
+ label={field.label}
179
+ />
180
+ );
181
+ case "switch":
182
+ return (
183
+ <SwitchGroup
184
+ inputId={sectionId + "-" + name}
185
+ checked={field.value}
186
+ onChange={(value) => onChange(name, value)}
187
+ label={field.label}
188
+ />
189
+ );
190
+ case "select":
191
+ return (
192
+ <div className={styles.formGroup}>
193
+ <label
194
+ className={styles.formLabel}
195
+ htmlFor={sectionId + "-" + name}
196
+ >
197
+ {field.label}
198
+ </label>
199
+ <Dropdown
200
+ inputId={sectionId + "-" + name}
201
+ value={field.selectedOptionName}
202
+ onChange={(e) => selectOnChange(name, e.value)}
203
+ options={field.options}
204
+ optionLabel="label"
205
+ optionValue="name"
206
+ />
207
+ </div>
208
+ );
209
+ }
210
+ })}
211
+ </div>
212
+ );
213
+ }
214
+
215
+ type ActivitySettingsOptionsDisplayProps = {
216
+ options:
217
+ | ActivitySettingsMultipleOptionsSection
218
+ | ActivitySettingsRadioOptionsSection;
219
+ sectionId: string;
220
+ };
221
+
222
+ export function ActivitySettingsOptionsDisplay({
223
+ options,
224
+ sectionId,
225
+ }: ActivitySettingsOptionsDisplayProps) {
226
+ return (
227
+ <div>
228
+ {options.type === "radio" ? (
229
+ <ActivitySettingsRadioDisplay options={options} sectionId={sectionId} />
230
+ ) : (
231
+ <ActivitySettingsCheckboxesDisplay
232
+ options={options}
233
+ sectionId={sectionId}
234
+ />
235
+ )}
236
+ </div>
237
+ );
238
+ }
239
+
240
+ type ActivitySettingsCheckboxesDisplayProps = {
241
+ options: ActivitySettingsMultipleOptionsSection;
242
+ sectionId: string;
243
+ };
244
+
245
+ function ActivitySettingsCheckboxesDisplay({
246
+ options,
247
+ sectionId,
248
+ }: ActivitySettingsCheckboxesDisplayProps) {
249
+ const dispatch = useAppDispatch();
250
+ const onChange = (optionName: string, checked: boolean) => {
251
+ dispatch(
252
+ updateSettings((oldSettings: ActivitySettings | undefined) => {
253
+ if (!oldSettings) {
254
+ return undefined;
255
+ }
256
+ const oldSection = oldSettings[
257
+ sectionId
258
+ ] as ActivitySettingsMultipleOptionsSection;
259
+ const newSettings = {
260
+ ...oldSettings,
261
+ [sectionId]: {
262
+ ...oldSection,
263
+ selectedOptionNames: checked
264
+ ? [...oldSection.selectedOptionNames, optionName]
265
+ : oldSection.selectedOptionNames.filter((n) => n !== optionName),
266
+ },
267
+ };
268
+ return newSettings as ActivitySettings;
269
+ }),
270
+ );
271
+ };
272
+ return (
273
+ <div className="sidebarRadioButtons">
274
+ {options.options.map((option) => (
275
+ <CheckboxGroup
276
+ key={option.name}
277
+ inputId={sectionId + "-" + option.name}
278
+ checked={options.selectedOptionNames.includes(option.name)}
279
+ onChange={(value) => onChange(option.name, value)}
280
+ label={option.label}
281
+ />
282
+ ))}
283
+ </div>
284
+ );
285
+ }
286
+
287
+ type ActivitySettingsRadioDisplayProps = {
288
+ options: ActivitySettingsRadioOptionsSection;
289
+ sectionId: string;
290
+ };
291
+
292
+ function ActivitySettingsRadioDisplay({
293
+ options,
294
+ sectionId,
295
+ }: ActivitySettingsRadioDisplayProps) {
296
+ const dispatch = useAppDispatch();
297
+ const onChange = (optionName: string, checked: boolean) => {
298
+ if (!checked) {
299
+ return;
300
+ }
301
+ dispatch(
302
+ updateSettings((oldSettings: ActivitySettings | undefined) => {
303
+ if (!oldSettings) {
304
+ return undefined;
305
+ }
306
+ const oldSection = oldSettings[
307
+ sectionId
308
+ ] as ActivitySettingsRadioOptionsSection;
309
+ const newSettings = {
310
+ ...oldSettings,
311
+ [sectionId]: {
312
+ ...oldSection,
313
+ selectedOptionName: checked ? optionName : null,
314
+ },
315
+ };
316
+ return newSettings as ActivitySettings;
317
+ }),
318
+ );
319
+ };
320
+ return (
321
+ <div className="sidebarRadioButtons">
322
+ {options.options.map((option) => (
323
+ <RadioGroup
324
+ key={option.name}
325
+ inputId={sectionId + "-" + option.name}
326
+ checked={options.selectedOptionName === option.name}
327
+ onChange={(value) => {
328
+ onChange(option.name, value);
329
+ }}
330
+ label={option.label}
331
+ />
332
+ ))}
333
+ </div>
334
+ );
335
+ }
336
+
337
+ type CheckboxGroupProps = {
338
+ inputId: string;
339
+ label: string;
340
+ checked: boolean;
341
+ onChange?: (value: boolean) => void;
342
+ };
343
+
344
+ const CheckboxGroup: React.FC<CheckboxGroupProps> = (props) => {
345
+ return (
346
+ <div className="sidebarRadioGroup">
347
+ <Checkbox
348
+ inputId={props.inputId}
349
+ checked={props.checked}
350
+ onChange={(e) =>
351
+ props.onChange && e.checked != null && props.onChange(e.checked)
352
+ }
353
+ />
354
+ <label className={styles.checkboxLabel} htmlFor={props.inputId}>
355
+ {props.label}
356
+ </label>
357
+ </div>
358
+ );
359
+ };
360
+
361
+ const RadioGroup: React.FC<CheckboxGroupProps> = (props) => {
362
+ return (
363
+ <div className="sidebarRadioGroup">
364
+ <RadioButton
365
+ inputId={props.inputId}
366
+ checked={props.checked}
367
+ onChange={(e) => {
368
+ props.onChange && e.checked != null && props.onChange(e.checked);
369
+ }}
370
+ />
371
+ <label className={styles.checkboxLabel} htmlFor={props.inputId}>
372
+ {props.label}
373
+ </label>
374
+ </div>
375
+ );
376
+ };
377
+
378
+ const SwitchGroup: React.FC<CheckboxGroupProps> = (props) => {
379
+ return (
380
+ <div className="sidebarRadioGroup">
381
+ <InputSwitch
382
+ inputId={props.inputId}
383
+ checked={props.checked}
384
+ onChange={(e) =>
385
+ props.onChange && e.checked != null && props.onChange(e.checked)
386
+ }
387
+ />
388
+ <label className={styles.checkboxLabel} htmlFor={props.inputId}>
389
+ {props.label}
390
+ </label>
391
+ </div>
392
+ );
393
+ };
@@ -110,7 +110,6 @@ const CapytaleMenu: React.FC = () => {
110
110
  label="Rendre"
111
111
  icon="pi pi-envelope"
112
112
  onClick={() => {
113
- console.log("Hey");
114
113
  confirmFinishAssignment();
115
114
  }}
116
115
  />
@@ -19,7 +19,6 @@ const GradingNav: React.FC = () => {
19
19
  const nid = useAppSelector(selectNid) as number;
20
20
  const mode = useAppSelector(selectMode);
21
21
  const activityNid = useAppSelector(selectActivityNid) as number;
22
- console.log("Le nid est : ", nid);
23
22
  useEffect(() => {
24
23
  evalApi.listSa(activityNid).then((j) => {
25
24
  setStudentList(j);
@@ -19,6 +19,7 @@ import {
19
19
  selectCanChoosePedagoLayout,
20
20
  selectCanChooseTheme,
21
21
  } from "../activityData/activityDataSlice";
22
+ import { ActivitySettingsDisplay } from "../activitySettings";
22
23
 
23
24
  const SidebarContent: FC = () => {
24
25
  const canChooseOrientation = useAppSelector(selectCanChoosePedagoLayout);
@@ -33,7 +34,7 @@ const SidebarContent: FC = () => {
33
34
  <Fieldset
34
35
  legend="Actions"
35
36
  className={classNames(
36
- styles.sidebarFieldset,
37
+ "sidebarFieldset",
37
38
  styles.sidebarFieldsetButtons,
38
39
  )}
39
40
  >
@@ -41,9 +42,9 @@ const SidebarContent: FC = () => {
41
42
  </Fieldset>
42
43
  )}
43
44
  {canChooseOrientation && (
44
- <Fieldset legend="Disposition" className={styles.sidebarFieldset}>
45
- <div className={styles.sidebarRadioButtons}>
46
- <div className={styles.sidebarRadioGroup}>
45
+ <Fieldset legend="Disposition" className="sidebarFieldset">
46
+ <div className="sidebarRadioButtons">
47
+ <div className="sidebarRadioGroup">
47
48
  <RadioButton
48
49
  inputId="rb-horizontal"
49
50
  name="horizontal"
@@ -55,7 +56,7 @@ const SidebarContent: FC = () => {
55
56
  Horizontale
56
57
  </label>
57
58
  </div>
58
- <div className={styles.sidebarRadioGroup}>
59
+ <div className="sidebarRadioGroup">
59
60
  <RadioButton
60
61
  inputId="rb-vertical"
61
62
  name="vertical"
@@ -71,9 +72,9 @@ const SidebarContent: FC = () => {
71
72
  </Fieldset>
72
73
  )}
73
74
  {canChooseTheme && (
74
- <Fieldset legend="Thème" className={styles.sidebarFieldset}>
75
- <div className={styles.sidebarRadioButtons}>
76
- <div className={styles.sidebarRadioGroup}>
75
+ <Fieldset legend="Thème" className="sidebarFieldset">
76
+ <div className="sidebarRadioButtons">
77
+ <div className="sidebarRadioGroup">
77
78
  <RadioButton
78
79
  inputId="rb-light"
79
80
  name="light"
@@ -88,7 +89,7 @@ const SidebarContent: FC = () => {
88
89
  Clair
89
90
  </label>
90
91
  </div>
91
- <div className={styles.sidebarRadioGroup}>
92
+ <div className="sidebarRadioGroup">
92
93
  <RadioButton
93
94
  inputId="rb-dark"
94
95
  name="dark"
@@ -103,7 +104,7 @@ const SidebarContent: FC = () => {
103
104
  Sombre
104
105
  </label>
105
106
  </div>
106
- <div className={styles.sidebarRadioGroup}>
107
+ <div className="sidebarRadioGroup">
107
108
  <RadioButton
108
109
  inputId="rb-auto"
109
110
  name="auto"
@@ -118,6 +119,7 @@ const SidebarContent: FC = () => {
118
119
  </div>
119
120
  </Fieldset>
120
121
  )}
122
+ <ActivitySettingsDisplay />
121
123
  </>
122
124
  );
123
125
  };
@@ -139,24 +139,8 @@
139
139
  align-items: center;
140
140
  }
141
141
 
142
- .sidebarFieldset {
143
- margin-bottom: 8px;
144
- }
145
-
146
142
  .sidebarFieldsetButtons :global(.p-fieldset-content) {
147
143
  display: flex;
148
144
  flex-direction: column;
149
145
  gap: 6px;
150
146
  }
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
- }
@@ -48,7 +48,6 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
48
48
  ) => {
49
49
  dispatch(setComments(event.target.value));
50
50
  dispatch(setIsMPDirty(true));
51
- console.log("51");
52
51
  };
53
52
 
54
53
  const handleGradingChange: ChangeEventHandler<HTMLTextAreaElement> = (
@@ -56,7 +55,6 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
56
55
  ) => {
57
56
  dispatch(setGrading(event.target.value));
58
57
  dispatch(setIsMPDirty(true));
59
- console.log("59");
60
58
  };
61
59
 
62
60
  return (
package/src/index.css CHANGED
@@ -33,7 +33,45 @@ body,
33
33
  color-scheme: light;
34
34
  }
35
35
 
36
+ .p-splitter-panel:has(> #meta-player-content-cover) {
37
+ position: relative;
38
+ }
39
+
40
+ #meta-player-content-cover {
41
+ display: none;
42
+ width: 100%;
43
+ height: 100%;
44
+ position: absolute;
45
+ top: 0;
46
+ left: 0;
47
+ z-index: 1000;
48
+ background-color: rgba(0, 0, 0, 0);
49
+ .p-splitter-resizing & {
50
+ display: block;
51
+ }
52
+ }
53
+
36
54
  #meta-player-content,
37
55
  :has(> #meta-player-content) {
38
56
  height: 100%;
57
+ background-color: var(--surface-a);
58
+ }
59
+
60
+ .sidebarFieldset {
61
+ margin-bottom: 8px;
62
+ & .p-button-label {
63
+ text-align: left;
64
+ }
65
+ }
66
+
67
+ .sidebarRadioButtons {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 4px;
71
+ }
72
+
73
+ .sidebarRadioGroup {
74
+ display: flex;
75
+ gap: 8px;
76
+ align-items: center;
39
77
  }
@@ -0,0 +1,32 @@
1
+ // https://medium.com/@stheodorejohn/javascript-object-deep-equality-comparison-in-javascript-7aa227e889d4
2
+
3
+ export function deepEqual(obj1: any, obj2: any): boolean {
4
+ // Base case: If both objects are identical, return true.
5
+ if (obj1 === obj2) {
6
+ return true;
7
+ }
8
+ // Check if both objects are objects and not null.
9
+ if (
10
+ typeof obj1 !== "object" ||
11
+ typeof obj2 !== "object" ||
12
+ obj1 === null ||
13
+ obj2 === null
14
+ ) {
15
+ return false;
16
+ }
17
+ // Get the keys of both objects.
18
+ const keys1 = Object.keys(obj1);
19
+ const keys2 = Object.keys(obj2);
20
+ // Check if the number of keys is the same.
21
+ if (keys1.length !== keys2.length) {
22
+ return false;
23
+ }
24
+ // Iterate through the keys and compare their values recursively.
25
+ for (const key of keys1) {
26
+ if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
27
+ return false;
28
+ }
29
+ }
30
+ // If all checks pass, the objects are deep equal.
31
+ return true;
32
+ }
@@ -1,35 +0,0 @@
1
- import { FC } from "react";
2
- import { useAppDispatch } from "../../app/hooks";
3
- import { setPlayerSettings } from "./activityDataSlice";
4
- import { setLayout } from "../layout/layoutSlice";
5
- import { setAutoSwitch, switchToTheme } from "../theming/themingSlice";
6
- import { MetaPlayerOptions } from "./metaPlayerOptions";
7
-
8
- type OptionSetterProps = {
9
- options: MetaPlayerOptions;
10
- };
11
-
12
- const OptionSetter: FC<OptionSetterProps> = ({ options }) => {
13
- const dispatch = useAppDispatch();
14
- if (!options.supportsLightTheme && !options.supportsDarkTheme) {
15
- throw new Error("At least one theme must be supported");
16
- }
17
- dispatch(setPlayerSettings(options));
18
- dispatch(
19
- setLayout(
20
- options.pedagoLayout === "horizontal" ||
21
- options.pedagoLayout === "default-horizontal"
22
- ? "horizontal"
23
- : "vertical",
24
- ),
25
- );
26
- if (options.supportsDarkTheme && options.supportsLightTheme) {
27
- dispatch(setAutoSwitch(true));
28
- } else {
29
- dispatch(setAutoSwitch(false));
30
- dispatch(switchToTheme(options.supportsLightTheme ? "light" : "dark"));
31
- }
32
- return null;
33
- };
34
-
35
- export default OptionSetter;