@capytale/meta-player 0.1.1 → 0.1.3

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.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -10,7 +10,7 @@ import { store } from "./app/store";
10
10
  import "./index.css";
11
11
  import ThemeSwitcher from "./features/theming/ThemeSwitcher";
12
12
  import { ActivityJSProvider } from "./features/activityJS/ActivityJSProvider";
13
- import { LoadOptions } from "./features/activityJS/hooks";
13
+ import { LoadOptions } from "./features/activityJS/internal-hooks";
14
14
  import { ErrorBoundary } from "./utils/ErrorBoundary";
15
15
  import Saver from "./features/activityJS/Saver";
16
16
 
@@ -4,6 +4,7 @@ import {
4
4
  ReactNode,
5
5
  createContext,
6
6
  useContext,
7
+ useEffect,
7
8
  useState,
8
9
  } from "react";
9
10
 
@@ -11,12 +12,14 @@ import {
11
12
  ActivitySessionLoaderReturnValue,
12
13
  LoadOptions,
13
14
  useActivitySessionLoader,
14
- } from "./hooks";
15
+ } from "./internal-hooks";
15
16
  import { getIdFromUrl } from "../../utils/activity";
16
17
  import { useAppDispatch } from "../../app/hooks";
17
18
  import ActivitySession from "@capytale/activity.js/activity/activitySession";
18
19
  import { setActivityJSData } from "../activityData/activityDataSlice";
19
20
 
21
+ import tracker from "@capytale/activity.js/backend/capytale/tracker";
22
+
20
23
  const ActivityJSContext = createContext<ActivitySessionLoaderReturnValue>({
21
24
  state: "loading",
22
25
  activitySession: null,
@@ -32,10 +35,14 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
32
35
  const activityId = getIdFromUrl();
33
36
  const dispatch = useAppDispatch();
34
37
 
38
+ useEffect(() => {
39
+ tracker.trackPageView();
40
+ }, []);
41
+
35
42
  const callback = (data: ActivitySession) => {
36
43
  //@ts-expect-error
37
44
  window.capy = data;
38
-
45
+
39
46
  const ab = data.activityBunch;
40
47
  /*
41
48
  console.log("Test accès");
@@ -87,6 +94,7 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
87
94
  workflow: ab.assignmentNode?.workflow,
88
95
  }),
89
96
  );
97
+ tracker.trackActivity(data);
90
98
  setLoaded(true);
91
99
  };
92
100
 
@@ -0,0 +1,23 @@
1
+ import { FC, useEffect } from "react";
2
+ import {
3
+ SaveCallback,
4
+ addBeforeReset,
5
+ removeBeforeReset,
6
+ } from "./saverSlice";
7
+ import { useAppDispatch } from "../../app/hooks";
8
+
9
+ type BeforeResetActionProps = SaveCallback;
10
+
11
+ const BeforeResetAction: FC<BeforeResetActionProps> = (props) => {
12
+ const dispatch = useAppDispatch();
13
+ useEffect(() => {
14
+ dispatch(addBeforeReset(props));
15
+ return () => {
16
+ // Remove the callback when the component is unmounted
17
+ dispatch(removeBeforeReset(props.name));
18
+ };
19
+ }, [props]);
20
+ return null;
21
+ };
22
+
23
+ export default BeforeResetAction;
@@ -1,93 +1,61 @@
1
- import { useEffect, useState, useRef } from "react";
2
-
3
- import { autoLoad } from "@capytale/activity.js";
4
-
5
- import type ActivitySession from "@capytale/activity.js/activity/activitySession/uni";
6
-
7
- import type { ActivityBunchOptions as LoadOptions } from "@capytale/activity.js/activity/activityBunch/uni/backend";
8
-
9
- export type { LoadOptions };
10
-
11
- export type ActivitySessionLoaderReturnValue = {
12
- state: "loading" | "loaded" | "error";
13
- activitySession: ActivitySession | null;
14
- error?: any;
1
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
2
+ import {
3
+ selectMode,
4
+ selectWorkflow,
5
+ setSaveState,
6
+ } from "../activityData/activityDataSlice";
7
+ import { useActivityJS } from "./ActivityJSProvider";
8
+ import { selectBeforeReset } from "./saverSlice";
9
+
10
+ type UseResetProps = {
11
+ onError: (errorMessage: string) => void;
15
12
  };
16
13
 
17
- /**
18
- * Un hook pour utiliser activity.js
19
- * Charge une activité de façon réactive.
20
- *
21
- * @param id id de l'activité à charger
22
- * @param loadOptions les options de chargement
23
- * @param callback une callback appelée avec l'activité chargée
24
- * @returns un objet contenant l'état de chargement de l'activité et l'activité dès qu'elle est chargée
25
- */
26
-
27
- export function useActivitySessionLoader(
28
- id: number,
29
- loadOptions?: LoadOptions,
30
- callback?: (activitySession: ActivitySession) => void,
31
- ): ActivitySessionLoaderReturnValue {
32
- const callbackW = useHandlerWrapper(callback);
33
- const [state, setState] = useState<ActivitySessionLoaderReturnValue>({
34
- state: "loading",
35
- activitySession: null,
36
- });
37
- useEffect(() => {
38
- let cancelled = false;
39
- autoLoad(loadOptions)
40
- .then((data) => {
41
- if (cancelled) return;
42
- callbackW(data);
43
- setState({
44
- state: "loaded",
45
- activitySession: data,
46
- });
47
- (window as any).capy = data;
48
- })
49
- .catch((error) => {
50
- if (cancelled) return;
51
- setState({
52
- state: "error",
53
- activitySession: null,
54
- error,
55
- });
56
- });
57
- return () => {
58
- cancelled = true;
59
- setState({
60
- state: "loading",
61
- activitySession: null,
62
- });
63
- };
64
- }, [id, loadOptions?.binaryDataType, loadOptions?.readOnly]);
65
- return state;
66
- }
67
-
68
- /**
69
- * Si un handler est passé en props à un composant, il peut changer. Il n'y a pas trop de raison mais
70
- * rien ne l'interdit.
71
- * Si un useEffect invoque ce handler, il faudrait que le handler figure dans les dépendances du useEffect.
72
- * Du coup, si le handler change, le useEffect est ré-exécuté.
73
- * Ce hook crée un wrapper immuable autour du handler passé en props. Ce wrapper ne change jamais et n'a donc
74
- * pas besoin de figurer dans les dépendances du useEffect.
75
- *
76
- * @param handler
77
- */
78
- export function useHandlerWrapper<H extends () => any>(
79
- handler?: H,
80
- ): () => ReturnType<H> | void;
81
- export function useHandlerWrapper<H extends (...args: any[]) => any>(
82
- handler?: H,
83
- ): (...p: Parameters<H>) => ReturnType<H> | void;
84
- export function useHandlerWrapper<H extends (...args: any[]) => any>(
85
- handler?: H,
86
- ): (...p: Parameters<H>) => ReturnType<H> | void {
87
- const handlerRef = useRef<H>();
88
- handlerRef.current = handler;
89
- return useRef((...p: Parameters<H>) => {
90
- if (null == handlerRef.current) return;
91
- return handlerRef.current(...p);
92
- }).current;
93
- }
14
+ export const useReset = (props: UseResetProps) => {
15
+ const dispatch = useAppDispatch();
16
+ const beforeReset = useAppSelector(selectBeforeReset);
17
+ const activityJs = useActivityJS();
18
+ const mode = useAppSelector(selectMode);
19
+ const workflow = useAppSelector(selectWorkflow);
20
+ if (mode !== "assignment" || workflow !== "current") {
21
+ return null;
22
+ }
23
+ return async () => {
24
+ if (!activityJs.activitySession) {
25
+ throw new Error("No activity session to reset");
26
+ }
27
+ for (const callback of Object.values(beforeReset)) {
28
+ try {
29
+ const v = callback();
30
+ if (v instanceof Promise) {
31
+ await v;
32
+ }
33
+ } catch (e) {
34
+ console.error("Error in beforeReset callback", e);
35
+ props.onError((e as any).toString());
36
+ return;
37
+ }
38
+ }
39
+ try {
40
+ dispatch(setSaveState("saving"));
41
+ const ab = activityJs.activitySession.activityBunch;
42
+ // @ts-expect-error
43
+ ab.assignmentNode.content.value = null;
44
+ // @ts-expect-error
45
+ ab.assignmentNode.binaryData.value = null;
46
+ try {
47
+ const saveData = await ab.save();
48
+ console.log("Save return data", saveData);
49
+ location.reload();
50
+ } catch (e) {
51
+ console.error("Error in reset", e);
52
+ props.onError((e as any).toString());
53
+ return;
54
+ }
55
+ dispatch(setSaveState("idle"));
56
+ } catch (e) {
57
+ console.error("Error in reset process", e);
58
+ props.onError((e as any).toString());
59
+ }
60
+ };
61
+ };
@@ -0,0 +1,93 @@
1
+ import { useEffect, useState, useRef } from "react";
2
+
3
+ import { autoLoad } from "@capytale/activity.js";
4
+
5
+ import type ActivitySession from "@capytale/activity.js/activity/activitySession/uni";
6
+
7
+ import type { ActivityBunchOptions as LoadOptions } from "@capytale/activity.js/activity/activityBunch/uni/backend";
8
+
9
+ export type { LoadOptions };
10
+
11
+ export type ActivitySessionLoaderReturnValue = {
12
+ state: "loading" | "loaded" | "error";
13
+ activitySession: ActivitySession | null;
14
+ error?: any;
15
+ };
16
+
17
+ /**
18
+ * Un hook pour utiliser activity.js
19
+ * Charge une activité de façon réactive.
20
+ *
21
+ * @param id id de l'activité à charger
22
+ * @param loadOptions les options de chargement
23
+ * @param callback une callback appelée avec l'activité chargée
24
+ * @returns un objet contenant l'état de chargement de l'activité et l'activité dès qu'elle est chargée
25
+ */
26
+
27
+ export function useActivitySessionLoader(
28
+ id: number,
29
+ loadOptions?: LoadOptions,
30
+ callback?: (activitySession: ActivitySession) => void,
31
+ ): ActivitySessionLoaderReturnValue {
32
+ const callbackW = useHandlerWrapper(callback);
33
+ const [state, setState] = useState<ActivitySessionLoaderReturnValue>({
34
+ state: "loading",
35
+ activitySession: null,
36
+ });
37
+ useEffect(() => {
38
+ let cancelled = false;
39
+ autoLoad(loadOptions)
40
+ .then((data) => {
41
+ if (cancelled) return;
42
+ callbackW(data);
43
+ setState({
44
+ state: "loaded",
45
+ activitySession: data,
46
+ });
47
+ (window as any).capy = data;
48
+ })
49
+ .catch((error) => {
50
+ if (cancelled) return;
51
+ setState({
52
+ state: "error",
53
+ activitySession: null,
54
+ error,
55
+ });
56
+ });
57
+ return () => {
58
+ cancelled = true;
59
+ setState({
60
+ state: "loading",
61
+ activitySession: null,
62
+ });
63
+ };
64
+ }, [id, loadOptions?.binaryDataType, loadOptions?.readOnly]);
65
+ return state;
66
+ }
67
+
68
+ /**
69
+ * Si un handler est passé en props à un composant, il peut changer. Il n'y a pas trop de raison mais
70
+ * rien ne l'interdit.
71
+ * Si un useEffect invoque ce handler, il faudrait que le handler figure dans les dépendances du useEffect.
72
+ * Du coup, si le handler change, le useEffect est ré-exécuté.
73
+ * Ce hook crée un wrapper immuable autour du handler passé en props. Ce wrapper ne change jamais et n'a donc
74
+ * pas besoin de figurer dans les dépendances du useEffect.
75
+ *
76
+ * @param handler
77
+ */
78
+ export function useHandlerWrapper<H extends () => any>(
79
+ handler?: H,
80
+ ): () => ReturnType<H> | void;
81
+ export function useHandlerWrapper<H extends (...args: any[]) => any>(
82
+ handler?: H,
83
+ ): (...p: Parameters<H>) => ReturnType<H> | void;
84
+ export function useHandlerWrapper<H extends (...args: any[]) => any>(
85
+ handler?: H,
86
+ ): (...p: Parameters<H>) => ReturnType<H> | void {
87
+ const handlerRef = useRef<H>();
88
+ handlerRef.current = handler;
89
+ return useRef((...p: Parameters<H>) => {
90
+ if (null == handlerRef.current) return;
91
+ return handlerRef.current(...p);
92
+ }).current;
93
+ }
@@ -11,11 +11,13 @@ export type SaveCallback = {
11
11
  export interface SaverState {
12
12
  beforeSave: { [key: string]: CallbackType };
13
13
  afterSave: { [key: string]: CallbackType };
14
+ beforeReset: { [key: string]: CallbackType };
14
15
  }
15
16
 
16
17
  const initialState: SaverState = {
17
18
  beforeSave: {},
18
19
  afterSave: {},
20
+ beforeReset: {},
19
21
  };
20
22
 
21
23
  // If you are not using async thunks you can use the standalone `createSlice`.
@@ -36,23 +38,42 @@ export const saverSlice = createAppSlice({
36
38
  state.afterSave[action.payload.name] = action.payload.callback;
37
39
  },
38
40
  ),
41
+ addBeforeReset: create.reducer(
42
+ (state, action: PayloadAction<SaveCallback>) => {
43
+ state.beforeReset[action.payload.name] = action.payload.callback;
44
+ },
45
+ ),
39
46
  removeBeforeSave: create.reducer((state, action: PayloadAction<string>) => {
40
47
  delete state.beforeSave[action.payload];
41
48
  }),
42
49
  removeAfterSave: create.reducer((state, action: PayloadAction<string>) => {
43
50
  delete state.afterSave[action.payload];
44
51
  }),
52
+ removeBeforeReset: create.reducer(
53
+ (state, action: PayloadAction<string>) => {
54
+ delete state.beforeReset[action.payload];
55
+ },
56
+ ),
45
57
  }),
46
58
  // You can define your selectors here. These selectors receive the slice
47
59
  // state as their first argument.
48
60
  selectors: {
49
61
  selectBeforeSave: (saver) => saver.beforeSave,
50
62
  selectAfterSave: (saver) => saver.afterSave,
63
+ selectBeforeReset: (saver) => saver.beforeReset,
51
64
  },
52
65
  });
53
66
 
54
67
  // Action creators are generated for each case reducer function.
55
- export const { addBeforeSave, removeBeforeSave, addAfterSave, removeAfterSave } = saverSlice.actions;
68
+ export const {
69
+ addBeforeSave,
70
+ removeBeforeSave,
71
+ addAfterSave,
72
+ removeAfterSave,
73
+ addBeforeReset,
74
+ removeBeforeReset,
75
+ } = saverSlice.actions;
56
76
 
57
77
  // Selectors returned by `slice.selectors` take the root state as their first argument.
58
- export const { selectBeforeSave, selectAfterSave } = saverSlice.selectors;
78
+ export const { selectBeforeSave, selectAfterSave, selectBeforeReset } =
79
+ saverSlice.selectors;
@@ -2,6 +2,7 @@ import { Button } from "primereact/button";
2
2
  import { SplitButton } from "primereact/splitbutton";
3
3
  import { ConfirmDialog, confirmDialog } from "primereact/confirmdialog";
4
4
  import { Toast } from "primereact/toast";
5
+ import { OverlayPanel } from "primereact/overlaypanel";
5
6
 
6
7
  import styles from "./style.module.scss";
7
8
  import { useMemo, useRef } from "react";
@@ -21,6 +22,8 @@ import {
21
22
  import { copyToClipboard } from "../../utils/clipboard";
22
23
  import settings from "../../settings";
23
24
  import { wf } from "@capytale/activity.js/activity/field/workflow";
25
+ import ButtonDoubleIcon from "../../utils/ButtonDoubleIcon";
26
+ import CardSelector from "../../utils/CardSelector";
24
27
 
25
28
  const CapytaleMenu: React.FC = () => {
26
29
  const dispatch = useAppDispatch();
@@ -56,6 +59,7 @@ const CapytaleMenu: React.FC = () => {
56
59
  accept: () => changeWorkflow("finished"),
57
60
  });
58
61
  };
62
+ const overlayPanelWorkflow = useRef<OverlayPanel>(null);
59
63
  return (
60
64
  <>
61
65
  <Toast ref={toast} position="bottom-right" />
@@ -144,60 +148,62 @@ const CapytaleMenu: React.FC = () => {
144
148
  />
145
149
  )}
146
150
  {mode === "review" && (
147
- <SplitButton
148
- label={
149
- workflow === "current"
150
- ? "En cours"
151
- : workflow === "finished"
152
- ? "Rendue"
153
- : "Corrigée"
154
- }
155
- severity="secondary"
156
- size="small"
157
- icon={
158
- workflow === "current"
159
- ? "pi pi-pencil"
160
- : workflow === "finished"
161
- ? "pi pi-envelope"
162
- : "pi pi-check-square"
163
- }
164
- outlined
165
- model={[
166
- ...(workflow === "current"
167
- ? []
168
- : [
169
- {
170
- label: "Réautoriser la modification",
171
- icon: "pi pi-pencil",
172
- command: () => {
173
- changeWorkflow("current");
174
- },
175
- },
176
- ]),
177
- ...(workflow === "finished"
178
- ? []
179
- : [
180
- {
181
- label: "Marquer comme rendue",
182
- icon: "pi pi-envelope",
183
- command: () => {
184
- changeWorkflow("finished");
185
- },
186
- },
187
- ]),
188
- ...(workflow === "corrected"
189
- ? []
190
- : [
191
- {
192
- label: "Marquer comme corrigée",
193
- icon: "pi pi-check-square",
194
- command: () => {
195
- changeWorkflow("corrected");
196
- },
197
- },
198
- ]),
199
- ]}
200
- />
151
+ <div>
152
+ <ButtonDoubleIcon
153
+ severity="secondary"
154
+ size="small"
155
+ outlined
156
+ onClick={(e) => {
157
+ overlayPanelWorkflow.current!.toggle(e);
158
+ }}
159
+ label={
160
+ workflow === "current"
161
+ ? "En cours"
162
+ : workflow === "finished"
163
+ ? "Rendue"
164
+ : "Corrigée"
165
+ }
166
+ leftIcon={
167
+ "pi " +
168
+ (workflow === "current"
169
+ ? "pi pi-pencil"
170
+ : workflow === "finished"
171
+ ? "pi pi-envelope"
172
+ : "pi pi-check-square")
173
+ }
174
+ rightIcon="pi pi-angle-down"
175
+ />
176
+ <OverlayPanel ref={overlayPanelWorkflow}>
177
+ <h3 className={styles.overlayPanelWorkflowTitle}>État de la copie</h3>
178
+ <CardSelector
179
+ selected={workflow}
180
+ onChange={(option: wf) => {
181
+ changeWorkflow(option);
182
+
183
+ overlayPanelWorkflow.current!.hide();
184
+ }}
185
+ options={[
186
+ {
187
+ value: "current",
188
+ title: "En cours",
189
+ description:
190
+ "L'élève peut modifier sa copie à tout moment.",
191
+ },
192
+ {
193
+ value: "finished",
194
+ title: "Rendue",
195
+ description: "L'élève ne peut plus modifier sa copie.",
196
+ },
197
+ {
198
+ value: "corrected",
199
+ title: "Corrigée",
200
+ description:
201
+ "L'élève ne peut plus modifier sa copie et a reçu une correction.",
202
+ },
203
+ ]}
204
+ />
205
+ </OverlayPanel>
206
+ </div>
201
207
  )}
202
208
  <ConfirmDialog />
203
209
  </div>
@@ -21,7 +21,9 @@ import {
21
21
  } from "../activityData/activityDataSlice";
22
22
  import { ActivitySettingsDisplay } from "../activitySettings";
23
23
  import { Button } from "primereact/button";
24
+ import { ConfirmPopup, confirmPopup } from "primereact/confirmpopup";
24
25
  import settings from "../../settings";
26
+ import { useReset } from "../activityJS/hooks";
25
27
 
26
28
  type SidebarContentProps = {
27
29
  showHelp?: () => void;
@@ -33,11 +35,12 @@ const SidebarContent: FC<SidebarContentProps> = (props) => {
33
35
  const orientation = useAppSelector(selectOrientation);
34
36
  const themeName = useAppSelector(selectThemeNameOrAuto);
35
37
  const sidebarActions = useAppSelector(selectSidebarActions);
38
+ const reset = useReset({ onError: console.error });
36
39
  const dispatch = useAppDispatch();
37
40
  return (
38
41
  <>
39
- {props.showHelp && (
40
- <div className={styles.sidebarCapytaleActions}>
42
+ <div className={styles.sidebarCapytaleActions}>
43
+ {props.showHelp && (
41
44
  <Button
42
45
  severity="secondary"
43
46
  size="small"
@@ -53,8 +56,38 @@ const SidebarContent: FC<SidebarContentProps> = (props) => {
53
56
  showDelay: settings.TOOLTIP_SHOW_DELAY,
54
57
  }}
55
58
  />
56
- </div>
57
- )}
59
+ )}
60
+ {reset && (
61
+ <>
62
+ <ConfirmPopup />
63
+ <Button
64
+ severity="secondary"
65
+ size="small"
66
+ icon={"pi pi-undo"}
67
+ onClick={(event) => {
68
+ confirmPopup({
69
+ target: event.currentTarget,
70
+ message:
71
+ "Toutes vos modifications seront perdues. Continuer ?",
72
+ icon: "pi pi-exclamation-triangle",
73
+ defaultFocus: "reject",
74
+ acceptClassName: "p-button-danger",
75
+ accept: reset,
76
+ acceptLabel: "Oui",
77
+ rejectLabel: "Non",
78
+ });
79
+ }}
80
+ outlined
81
+ label="Réinitialiser l'activité"
82
+ tooltip="Revenir à la version de l'enseignant"
83
+ tooltipOptions={{
84
+ position: "left",
85
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
86
+ }}
87
+ />
88
+ </>
89
+ )}
90
+ </div>
58
91
  {sidebarActions.length > 0 && (
59
92
  <Fieldset
60
93
  legend="Actions"
@@ -147,7 +147,14 @@
147
147
 
148
148
  .sidebarCapytaleActions {
149
149
  margin-bottom: 8px;
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 8px;
150
153
  & > button {
151
154
  width: 100%;
152
155
  }
153
156
  }
157
+
158
+ .overlayPanelWorkflowTitle {
159
+ margin-top: 0;
160
+ }
@@ -0,0 +1,35 @@
1
+ import { Button, ButtonProps } from "primereact/button";
2
+ import React from "react";
3
+
4
+ type ButtonDoubleIconProps = Exclude<
5
+ ButtonProps,
6
+ "icon" | "children" | "iconPos"
7
+ > & {
8
+ leftIcon: string;
9
+ rightIcon: string;
10
+ };
11
+
12
+ const ButtonDoubleIcon: React.FC<ButtonDoubleIconProps> = function ({
13
+ label,
14
+ leftIcon,
15
+ rightIcon,
16
+ ...props
17
+ }) {
18
+ return (
19
+ <Button {...props}>
20
+ <span
21
+ className={"p-button-icon p-c p-button-icon-left " + leftIcon}
22
+ data-pc-section="icon"
23
+ ></span>
24
+ <span className="p-button-label p-c" data-pc-section="label">
25
+ {label}
26
+ </span>
27
+ <span
28
+ className={"p-button-icon p-c p-button-icon-right " + rightIcon}
29
+ data-pc-section="icon"
30
+ ></span>
31
+ </Button>
32
+ );
33
+ };
34
+
35
+ export default ButtonDoubleIcon;
@@ -0,0 +1,39 @@
1
+ import { RadioButton } from "primereact/radiobutton";
2
+ import styles from "./style.module.scss";
3
+
4
+ type CardSelectorProps<ValueType> = {
5
+ options: { title: string; description: string; value: ValueType }[];
6
+ selected?: ValueType | null;
7
+ onChange: (value: ValueType) => void | Promise<void>;
8
+ };
9
+ const CardSelector = function <ValueType>(props: CardSelectorProps<ValueType>) {
10
+ return (
11
+ <>
12
+ <div className={styles.cardSelector}>
13
+ {props.options.map((option) => (
14
+ <div
15
+ className={styles.cardSelectorOption}
16
+ data-selected={props.selected === option.value}
17
+ onClick={() => {
18
+ props.onChange(option.value);
19
+ }}
20
+ >
21
+ <div className={styles.cardSelectorOptionRadio}>
22
+ <RadioButton checked={props.selected === option.value} />
23
+ </div>
24
+ <div className={styles.cardSelectorOptionContent}>
25
+ <div className={styles.cardSelectorOptionTitle}>
26
+ {option.title}
27
+ </div>
28
+ <div className={styles.cardSelectorOptionDescription}>
29
+ {option.description}
30
+ </div>
31
+ </div>
32
+ </div>
33
+ ))}
34
+ </div>
35
+ </>
36
+ );
37
+ };
38
+
39
+ export default CardSelector;
@@ -0,0 +1,46 @@
1
+ .cardSelector {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 1.25rem;
5
+ & :global(.p-card-title) {
6
+ font-size: 1.2rem;
7
+ }
8
+
9
+ & :global(.p-card-content) {
10
+ padding: 0.5rem 0;
11
+ }
12
+
13
+ & p {
14
+ margin: 0;
15
+ }
16
+ }
17
+
18
+ .cardSelectorOption {
19
+ padding: 1.25rem;
20
+ display: flex;
21
+ gap: 1.25rem;
22
+ background-color: var(--surface-100);
23
+ box-shadow: none;
24
+ border: 1px solid var(--surface-300);
25
+ border-radius: 6px;
26
+ cursor: pointer;
27
+ transition: background-color 0.2s;
28
+ &[data-selected="true"] {
29
+ background-color: var(--surface-0);
30
+ border-color: var(--primary-color);
31
+ }
32
+ &:hover {
33
+ background-color: var(--surface-0);
34
+ }
35
+ }
36
+
37
+ .cardSelectorOptionRadio {
38
+ display: flex;
39
+ align-items: center;
40
+ }
41
+
42
+ .cardSelectorOptionTitle {
43
+ font-size: 1.2rem;
44
+ font-weight: 700;
45
+ margin-bottom: 0.5rem;
46
+ }