@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
package/src/App.tsx ADDED
@@ -0,0 +1,116 @@
1
+ import { Splitter, SplitterPanel } from "primereact/splitter";
2
+ import Navbar from "./features/navbar";
3
+
4
+ import styles from "./app.module.scss";
5
+ import Pedago from "./features/pedago";
6
+ import { useAppDispatch, useAppSelector } from "./app/hooks";
7
+ import { selectThemeIsDark } from "./features/theming/themingSlice";
8
+
9
+ import { Ripple } from "primereact/ripple";
10
+ import { classNames } from "primereact/utils";
11
+ import {
12
+ selectIsPedagoVisible,
13
+ selectOrientation,
14
+ toggleIsPedagoVisible,
15
+ } from "./features/layout/layoutSlice";
16
+ import { FC, PropsWithChildren } from "react";
17
+ import { Tooltip } from "primereact/tooltip";
18
+ import {
19
+ selectHasGradingOrComments,
20
+ selectHasInstructions,
21
+ selectMode,
22
+ } from "./features/activityData/activityDataSlice";
23
+ import settings from "./settings";
24
+ import ReviewNavbar from "./features/navbar/ReviewNavbar";
25
+
26
+ type AppProps = PropsWithChildren<{}>;
27
+
28
+ const App: FC<AppProps> = (props) => {
29
+ const isDark = useAppSelector(selectThemeIsDark) as boolean;
30
+ const mode = useAppSelector(selectMode);
31
+ const isHorizontal = useAppSelector(selectOrientation) === "horizontal";
32
+ const isPedagoVisible = useAppSelector(selectIsPedagoVisible) as boolean;
33
+ const hasInstructions = useAppSelector(selectHasInstructions);
34
+ const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
35
+ const hasPedago = hasInstructions || hasGradingOrComments;
36
+ const dispatch = useAppDispatch();
37
+ return (
38
+ <div
39
+ className={classNames(
40
+ styles.app,
41
+ isDark ? "dark-theme" : "light-theme",
42
+ isHorizontal ? "layout-horizontal" : "layout-vertical",
43
+ )}
44
+ >
45
+ <div>
46
+ <Navbar />
47
+ {mode === "review" && <ReviewNavbar />}
48
+ </div>
49
+ {hasPedago && isPedagoVisible && (
50
+ <Splitter
51
+ className={styles.appPedagoSplitter}
52
+ layout={isHorizontal ? "vertical" : "horizontal"}
53
+ >
54
+ <SplitterPanel minSize={15} size={30} className={styles.pedagoPanel}>
55
+ <Pedago key="pedago" />
56
+ </SplitterPanel>
57
+ <SplitterPanel minSize={40} size={70}>
58
+ {props.children}
59
+ </SplitterPanel>
60
+ </Splitter>
61
+ )}
62
+ {(!hasPedago || !isPedagoVisible) && (
63
+ <div className={styles.hiddenPedagoContainer}>
64
+ <div
65
+ className={classNames(
66
+ styles.hiddenPedago,
67
+ hasPedago ? null : styles.noPedago,
68
+ )}
69
+ >
70
+ <div
71
+ className={classNames(
72
+ styles.hiddenPedagoButton,
73
+ hasPedago ? "p-ripple" : null,
74
+ )}
75
+ onClick={
76
+ hasPedago ? () => dispatch(toggleIsPedagoVisible()) : undefined
77
+ }
78
+ data-pr-tooltip={
79
+ hasPedago
80
+ ? "Afficher les consignes"
81
+ : "Pas de consignes ni de note"
82
+ }
83
+ aria-label={
84
+ hasPedago
85
+ ? "Afficher les consignes"
86
+ : "Pas de consignes ni de note"
87
+ }
88
+ role={hasPedago ? "button" : "note"}
89
+ >
90
+ <i
91
+ className={classNames(
92
+ "pi",
93
+ hasPedago
94
+ ? isHorizontal
95
+ ? "pi-angle-double-down"
96
+ : "pi-angle-double-right"
97
+ : "pi-minus-circle",
98
+ )}
99
+ />
100
+ <Ripple />
101
+ </div>
102
+ <Tooltip
103
+ target={"." + styles.hiddenPedagoButton}
104
+ showDelay={settings.TOOLTIP_SHOW_DELAY}
105
+ position={isHorizontal ? "bottom" : "right"}
106
+ mouseTrack={!isHorizontal}
107
+ />
108
+ </div>
109
+ <div className={styles.hiddenPedagoContent}>{props.children}</div>
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ };
115
+
116
+ export default App;
@@ -0,0 +1,39 @@
1
+ .App {
2
+ text-align: center;
3
+ }
4
+
5
+ .App-logo {
6
+ height: 40vmin;
7
+ pointer-events: none;
8
+ }
9
+
10
+ @media (prefers-reduced-motion: no-preference) {
11
+ .App-logo {
12
+ animation: App-logo-float infinite 3s ease-in-out;
13
+ }
14
+ }
15
+
16
+ .App-header {
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ justify-content: center;
22
+ font-size: calc(10px + 2vmin);
23
+ }
24
+
25
+ .App-link {
26
+ color: rgb(112, 76, 182);
27
+ }
28
+
29
+ @keyframes App-logo-float {
30
+ 0% {
31
+ transform: translateY(0);
32
+ }
33
+ 50% {
34
+ transform: translateY(10px);
35
+ }
36
+ 100% {
37
+ transform: translateY(0px);
38
+ }
39
+ }
@@ -0,0 +1,51 @@
1
+ import React, { PropsWithChildren, useMemo } from "react";
2
+
3
+ import { Provider } from "react-redux";
4
+
5
+ import { APIOptions, PrimeReactProvider } from "primereact/api";
6
+ import "primeicons/primeicons.css";
7
+
8
+ import App from "./App";
9
+ import { store } from "./app/store";
10
+ import "./index.css";
11
+ import ThemeSwitcher from "./features/theming/ThemeSwitcher";
12
+ import { ActivityJSProvider } from "./features/activityJS/ActivityJSProvider";
13
+ import { LoadOptions } from "./features/activityJS/hooks";
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
+ import Saver from "./features/activityJS/Saver";
21
+
22
+ type MetaPlayerProps = PropsWithChildren<{
23
+ activityJSOptions?: LoadOptions;
24
+ options?: Partial<MetaPlayerOptions>;
25
+ }>;
26
+
27
+ const MetaPlayer: React.FC<MetaPlayerProps> = (props) => {
28
+ const primeSettings: Partial<APIOptions> = {
29
+ ripple: true,
30
+ };
31
+ const options = useMemo(
32
+ () => ({ ...defaultMetaPlayerOptions, ...props.options }),
33
+ [props.options],
34
+ );
35
+ return (
36
+ <PrimeReactProvider value={primeSettings}>
37
+ <Provider store={store}>
38
+ <OptionSetter options={options} />
39
+ <ThemeSwitcher />
40
+ <ErrorBoundary fallback={<div>Une erreur est survenue</div>}>
41
+ <ActivityJSProvider options={props.activityJSOptions}>
42
+ <Saver />
43
+ <App>{props.children}</App>
44
+ </ActivityJSProvider>
45
+ </ErrorBoundary>
46
+ </Provider>
47
+ </PrimeReactProvider>
48
+ );
49
+ };
50
+
51
+ export default MetaPlayer;
@@ -0,0 +1,6 @@
1
+ import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit"
2
+
3
+ // `buildCreateSlice` allows us to create a slice with async thunks.
4
+ export const createAppSlice = buildCreateSlice({
5
+ creators: { asyncThunk: asyncThunkCreator },
6
+ })
@@ -0,0 +1,12 @@
1
+ // This file serves as a central hub for re-exporting pre-typed Redux hooks.
2
+ // These imports are restricted elsewhere to ensure consistent
3
+ // usage of typed hooks throughout the application.
4
+ // We disable the ESLint rule here because this is the designated place
5
+ // for importing and re-exporting the typed versions of hooks.
6
+ /* eslint-disable @typescript-eslint/no-restricted-imports */
7
+ import { useDispatch, useSelector } from "react-redux"
8
+ import type { AppDispatch, RootState } from "./store"
9
+
10
+ // Use throughout your app instead of plain `useDispatch` and `useSelector`
11
+ export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
12
+ export const useAppSelector = useSelector.withTypes<RootState>()
@@ -0,0 +1,46 @@
1
+ import type { Action, ThunkAction } from "@reduxjs/toolkit";
2
+ import { combineSlices, configureStore } from "@reduxjs/toolkit";
3
+ import { setupListeners } from "@reduxjs/toolkit/query";
4
+ import { themingSlice } from "../features/theming/themingSlice";
5
+ import { layoutSlice } from "../features/layout/layoutSlice";
6
+ import { activityDataSlice } from "../features/activityData/activityDataSlice";
7
+ import { navbarSlice } from "../features/navbar/navbarSlice";
8
+ import { saverSlice } from "../features/activityJS/saverSlice";
9
+
10
+ // `combineSlices` automatically combines the reducers using
11
+ // their `reducerPath`s, therefore we no longer need to call `combineReducers`.
12
+ const rootReducer = combineSlices(
13
+ activityDataSlice,
14
+ themingSlice,
15
+ layoutSlice,
16
+ navbarSlice,
17
+ saverSlice,
18
+ );
19
+ // Infer the `RootState` type from the root reducer
20
+ export type RootState = ReturnType<typeof rootReducer>;
21
+
22
+ // The store setup is wrapped in `makeStore` to allow reuse
23
+ // when setting up tests that need the same store config
24
+ export const makeStore = (preloadedState?: Partial<RootState>) => {
25
+ const store = configureStore({
26
+ reducer: rootReducer,
27
+ preloadedState,
28
+ });
29
+ // configure listeners using the provided defaults
30
+ // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors
31
+ setupListeners(store.dispatch);
32
+ return store;
33
+ };
34
+
35
+ export const store = makeStore();
36
+
37
+ // Infer the type of `store`
38
+ export type AppStore = typeof store;
39
+ // Infer the `AppDispatch` type from the store itself
40
+ export type AppDispatch = AppStore["dispatch"];
41
+ export type AppThunk<ThunkReturnType = void> = ThunkAction<
42
+ ThunkReturnType,
43
+ RootState,
44
+ unknown,
45
+ Action
46
+ >;
@@ -0,0 +1,56 @@
1
+ .app {
2
+ height: 100%;
3
+ display: grid;
4
+ grid-template-rows: auto 1fr;
5
+ }
6
+
7
+ .appPedagoSplitter {
8
+ height: 100%;
9
+ border: none;
10
+ border-radius: 0;
11
+ overflow-y: hidden;
12
+ }
13
+
14
+ .pedagoPanel {
15
+ width: 100%;
16
+ height: 100%;
17
+ overflow-y: hidden;
18
+ }
19
+
20
+ .hiddenPedagoContainer {
21
+ display: grid;
22
+ :global(.layout-horizontal) & {
23
+ grid-template-columns: 1fr;
24
+ grid-template-rows: 1.2rem 1fr;
25
+ }
26
+ :global(.layout-vertical) & {
27
+ grid-template-rows: 1fr;
28
+ grid-template-columns: 1.2rem 1fr;
29
+ }
30
+ }
31
+
32
+ .hiddenPedago {
33
+ background-color: var(--surface-200);
34
+ }
35
+
36
+ .hiddenPedagoButton {
37
+ width: 100%;
38
+ height: 100%;
39
+ display: flex;
40
+ justify-content: center;
41
+ align-items: center;
42
+ cursor: pointer;
43
+ & > i {
44
+ font-size: 1rem;
45
+ }
46
+ &:hover {
47
+ background-color: rgba(128, 128, 128, 0.05);
48
+ }
49
+ &:active {
50
+ background-color: rgba(128, 128, 128, 0.2);
51
+ }
52
+ }
53
+
54
+ .hiddenPedagoContent {
55
+ background-color: var(--surface-a);
56
+ }
package/src/demo.tsx ADDED
@@ -0,0 +1,81 @@
1
+ import React, { FC } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ import { MetaPlayer } from ".";
5
+ import { ActivityQuickActionsSetter } from "./features/navbar/ActivityQuickActions";
6
+ import { ActivitySidebarActionsSetter } from "./features/navbar/ActivitySidebarActions";
7
+ // import { useActivityJS } from "./features/activityJS/ActivityJSProvider";
8
+ import BeforeSaveAction from "./features/activityJS/BeforeSaveAction";
9
+
10
+ const DemoActivity: FC = () => {
11
+ // const activityJs = useActivityJS();
12
+ return (
13
+ <>
14
+ <ActivityQuickActionsSetter
15
+ actions={[
16
+ {
17
+ title: "Pause",
18
+ icon: "pi pi-pause",
19
+ action: () => console.log("Pause"),
20
+ },
21
+ {
22
+ title: "Play",
23
+ icon: "pi pi-play",
24
+ action: () => console.log("Play"),
25
+ },
26
+ ]}
27
+ />
28
+ <ActivitySidebarActionsSetter
29
+ actions={[
30
+ {
31
+ title: "Pause",
32
+ icon: "pi pi-pause",
33
+ action: () => console.log("Pause"),
34
+ },
35
+ {
36
+ title: "Play",
37
+ icon: "pi pi-play",
38
+ action: () => console.log("Play"),
39
+ },
40
+ ]}
41
+ />
42
+ <BeforeSaveAction
43
+ name="demo"
44
+ callback={() => console.log("Before save")}
45
+ />
46
+ <BeforeSaveAction
47
+ name="demo2"
48
+ callback={() => {
49
+ console.log("Before save 2");
50
+ // throw new Error("Error in before save 2");
51
+ }}
52
+ />
53
+ Activité
54
+ </>
55
+ );
56
+ };
57
+
58
+ const container = document.getElementById("root");
59
+
60
+ if (container) {
61
+ const root = createRoot(container);
62
+
63
+ root.render(
64
+ <React.StrictMode>
65
+ <MetaPlayer
66
+ options={{
67
+ hasInstructions: true,
68
+ pedagoLayout: "default-horizontal",
69
+ supportsLightTheme: true,
70
+ supportsDarkTheme: true,
71
+ }}
72
+ >
73
+ <DemoActivity />
74
+ </MetaPlayer>
75
+ </React.StrictMode>,
76
+ );
77
+ } else {
78
+ throw new Error(
79
+ "Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.",
80
+ );
81
+ }
@@ -0,0 +1,17 @@
1
+ import { FC, useEffect } from "react";
2
+ import { setIsDirty } from "./activityDataSlice";
3
+ import { useAppDispatch } from "../../app/hooks";
4
+
5
+ type IsDirtySetterProps = {
6
+ isDirty: boolean;
7
+ };
8
+
9
+ const IsDirtySetter: FC<IsDirtySetterProps> = (props) => {
10
+ const dispatch = useAppDispatch();
11
+ useEffect(() => {
12
+ dispatch(setIsDirty(props.isDirty));
13
+ }, [props]);
14
+ return null;
15
+ };
16
+
17
+ export default IsDirtySetter;
@@ -0,0 +1,35 @@
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;