@adriansteffan/reactive 0.0.26 → 0.0.27

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.
@@ -0,0 +1,387 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { ExperimentConfig, Store, Param, now } from '../utils/common';
4
+ import { useCallback, useEffect, useMemo, useRef, useState, ComponentType } from 'react';
5
+ import {
6
+ compileTimeline,
7
+ TimelineItem,
8
+ RefinedTrialData,
9
+ ComponentResultData,
10
+ } from '../utils/bytecode';
11
+
12
+ import Upload from './upload';
13
+ import Text from './text';
14
+ import PlainInput from './plaininput';
15
+ import ProlificEnding from './prolificending';
16
+ import Quest from './quest';
17
+ import EnterFullscreen from './enterfullscreen';
18
+ import ExitFullscreen from './exitfullscreen';
19
+ import MicrophoneCheck from './microphonecheck';
20
+ import RequestFilePermission from './mobilefilepermission';
21
+ import CanvasBlock from './canvasblock';
22
+ import CheckDevice from './checkdevice';
23
+
24
+ import VoicerecorderQuestionComponent from './voicerecorder';
25
+ import React from 'react';
26
+
27
+ type ComponentsMap = {
28
+ [key: string]: ComponentType<any>;
29
+ };
30
+
31
+ const defaultComponents: ComponentsMap = {
32
+ Text,
33
+ ProlificEnding,
34
+ EnterFullscreen,
35
+ ExitFullscreen,
36
+ Quest,
37
+ Upload,
38
+ MicrophoneCheck,
39
+ PlainInput,
40
+ RequestFilePermission,
41
+ CanvasBlock,
42
+ CheckDevice,
43
+ };
44
+
45
+ const defaultCustomQuestions: ComponentsMap = {
46
+ voicerecorder: VoicerecorderQuestionComponent,
47
+ };
48
+
49
+ interface RuntimeComponentContent {
50
+ name?: string;
51
+ type: string;
52
+ collectRefreshRate?: boolean;
53
+ hideSettings?: string[] | boolean;
54
+ props?: Record<string, any> | ((store: Store, data: RefinedTrialData[]) => Record<string, any>);
55
+ }
56
+
57
+ function isRuntimeComponentContent(content: any): content is RuntimeComponentContent {
58
+ return typeof content === 'object' && content !== null && typeof content.type === 'string';
59
+ }
60
+
61
+ export default function ExperimentRunner({
62
+ timeline,
63
+ config = {
64
+ showProgressBar: false,
65
+ },
66
+ components = {},
67
+ questions = {},
68
+ }: {
69
+ timeline: TimelineItem[];
70
+ config?: ExperimentConfig;
71
+ components?: ComponentsMap;
72
+ questions?: ComponentsMap;
73
+ }) {
74
+ const trialByteCode = useMemo(() => {
75
+ return compileTimeline(timeline);
76
+ }, [timeline]);
77
+
78
+ const [instructionPointer, setInstructionPointer] = useState(0);
79
+ const [data, setData] = useState<RefinedTrialData[]>(() => {
80
+ const urlParams: Record<string, any> = {};
81
+ const searchParams = new URLSearchParams(window.location.search);
82
+ for (const [key, value] of searchParams.entries()) {
83
+ urlParams[key] = value;
84
+ }
85
+
86
+ const registry = Param.getRegistry() || [];
87
+
88
+ const params: Record<string, any> = {};
89
+
90
+ // First add URL params (these will be overwritten by registry entries if there are name collisions)
91
+ for (const [key, value] of Object.entries(urlParams)) {
92
+ params[key] = {
93
+ value,
94
+ registered: false,
95
+ defaultValue: undefined,
96
+ type: undefined,
97
+ description: undefined,
98
+ };
99
+ }
100
+
101
+ // Then add/overwrite with registry entries
102
+ for (const param of registry) {
103
+ params[param.name] = {
104
+ value:
105
+ param.value !== undefined ? param.value : urlParams[param.name],
106
+ registered: true,
107
+ defaultValue: param.defaultValue,
108
+ type: param.type,
109
+ description: param.description,
110
+ };
111
+ }
112
+
113
+ const initialData: ComponentResultData = {
114
+ index: -1,
115
+ trialNumber: -1,
116
+ start: now(),
117
+ end: now(),
118
+ duration: 0,
119
+ type: '',
120
+ name: '',
121
+ responseData: {
122
+ userAgent: navigator.userAgent,
123
+ params,
124
+ },
125
+ };
126
+
127
+ return [initialData];
128
+ });
129
+
130
+ const [totalTrialsCompleted, setTotalTrialsCompleted] = useState(0);
131
+ const lastTrialEndTimeRef = useRef(now());
132
+ const experimentStoreRef = useRef<Store>({});
133
+
134
+ const componentsMap = { ...defaultComponents, ...components };
135
+ const customQuestionsMap: ComponentsMap = { ...defaultCustomQuestions, ...questions };
136
+
137
+ const progress = useMemo(() => {
138
+ const totalInstructions = trialByteCode.instructions.length;
139
+ return totalInstructions > 0 ? instructionPointer / totalInstructions : 0;
140
+ }, [instructionPointer, trialByteCode.instructions]);
141
+
142
+ useEffect(() => {
143
+ window.scrollTo(0, 0);
144
+ }, [instructionPointer]);
145
+
146
+ function updateStore(update: Partial<Store>) {
147
+ experimentStoreRef.current = { ...experimentStoreRef.current, ...update };
148
+ }
149
+
150
+ function next(
151
+ componentResponseData?: object,
152
+ actualStartTime?: number,
153
+ actualEndTime?: number,
154
+ ): void {
155
+ const currentTime = now();
156
+ const currentInstruction = trialByteCode.instructions[instructionPointer];
157
+
158
+ // Use the provided startTime if available, otherwise use the lastTrialEndTimeRef
159
+ const startTime = actualStartTime !== undefined ? actualStartTime : lastTrialEndTimeRef.current;
160
+
161
+ const endTime = actualEndTime !== undefined ? actualEndTime : currentTime;
162
+
163
+ if (currentInstruction?.type === 'ExecuteContent') {
164
+ const content = currentInstruction.content;
165
+ if (isRuntimeComponentContent(content)) {
166
+ const trialData: ComponentResultData = {
167
+ index: instructionPointer,
168
+ trialNumber: totalTrialsCompleted + 1,
169
+ start: startTime,
170
+ end: endTime,
171
+ duration: endTime - startTime,
172
+ type: content.type,
173
+ name: content.name ?? '',
174
+ responseData: componentResponseData,
175
+ };
176
+ setData((prevData) => [...prevData, trialData]);
177
+ setTotalTrialsCompleted((prevCount) => prevCount + 1);
178
+ } else {
179
+ console.log(
180
+ "ExecuteContent finished, but content wasn't standard component format:",
181
+ content,
182
+ );
183
+ }
184
+ }
185
+
186
+ let nextPointer = instructionPointer + 1;
187
+ let foundNextContent = false;
188
+
189
+ while (nextPointer < trialByteCode.instructions.length) {
190
+ const nextInstruction = trialByteCode.instructions[nextPointer];
191
+
192
+ switch (nextInstruction.type) {
193
+ case 'IfGoto':
194
+ if (nextInstruction.cond(experimentStoreRef.current, data)) {
195
+ const markerIndex = trialByteCode.markers[nextInstruction.marker];
196
+ if (markerIndex !== undefined) {
197
+ nextPointer = markerIndex;
198
+ continue;
199
+ } else {
200
+ console.error(`Marker ${nextInstruction.marker} not found`);
201
+ nextPointer++;
202
+ }
203
+ } else {
204
+ nextPointer++;
205
+ }
206
+ break;
207
+
208
+ case 'UpdateStore':
209
+ updateStore(nextInstruction.fun(experimentStoreRef.current, data));
210
+ nextPointer++;
211
+ break;
212
+
213
+ case 'ExecuteContent':
214
+ foundNextContent = true;
215
+ lastTrialEndTimeRef.current = now();
216
+ setInstructionPointer(nextPointer);
217
+ return;
218
+
219
+ default:
220
+ console.error('Unknown instruction type encountered:', nextInstruction);
221
+ nextPointer++;
222
+ break;
223
+ }
224
+ }
225
+
226
+ if (!foundNextContent) {
227
+ setInstructionPointer(nextPointer);
228
+ }
229
+ }
230
+
231
+ const collectRefreshRate = useCallback((callback: (refreshRate: number | null) => void) => {
232
+ let frameCount = 0;
233
+ const startTime = now();
234
+ const maxDuration = 20000;
235
+ let rafId: number;
236
+ let lastUpdateTime = startTime;
237
+ const updateInterval = 1000;
238
+
239
+ const calculateRefreshRate = () => {
240
+ const currentTime = now();
241
+ const elapsedTime = currentTime - startTime;
242
+ if (elapsedTime > 0) {
243
+ const refreshRate = Math.round((frameCount * 1000) / elapsedTime);
244
+ return refreshRate;
245
+ }
246
+ return null;
247
+ };
248
+
249
+ const cleanup = () => {
250
+ if (rafId) {
251
+ cancelAnimationFrame(rafId);
252
+ }
253
+ };
254
+
255
+ const countFrames = (timestamp: number) => {
256
+ frameCount++;
257
+ const elapsedTime = timestamp - startTime;
258
+
259
+ // Check if it's time for an update (once per second)
260
+ if (timestamp - lastUpdateTime >= updateInterval) {
261
+ callback(calculateRefreshRate());
262
+ lastUpdateTime = timestamp;
263
+ }
264
+
265
+ if (elapsedTime < maxDuration) {
266
+ rafId = requestAnimationFrame(countFrames);
267
+ } else {
268
+ cleanup();
269
+ callback(calculateRefreshRate());
270
+ }
271
+ };
272
+
273
+ rafId = requestAnimationFrame(countFrames);
274
+
275
+ return cleanup;
276
+ }, []);
277
+
278
+ const TimingWrapper: React.FC<{
279
+ children: React.ReactNode;
280
+ collectRefreshRate?: boolean;
281
+ }> = ({ children, collectRefreshRate: shouldcollect }) => {
282
+ const wrapperStartTimeRef = useRef<number | null>(null);
283
+ const refreshRateRef = useRef<number | null>(null);
284
+
285
+ useEffect(() => {
286
+ wrapperStartTimeRef.current = now();
287
+
288
+ if (shouldcollect) {
289
+ const cleanup = collectRefreshRate((rate) => {
290
+ refreshRateRef.current = rate;
291
+ updateStore({ _reactiveScreenRefreshRate: rate });
292
+ });
293
+
294
+ return cleanup;
295
+ }
296
+ }, []);
297
+
298
+ const childWithTimingProps = React.Children.map(children, (child) => {
299
+ if (React.isValidElement(child)) {
300
+ const interceptedNext = (
301
+ responseData?: object,
302
+ actualStartTime?: number,
303
+ actualEndTime?: number,
304
+ ) => {
305
+ const currentTimeWrapper = now();
306
+
307
+ // Use the timings provided by the component if available, otherwise use the one by the wrapper
308
+ const startTime =
309
+ actualStartTime !== undefined ? actualStartTime : wrapperStartTimeRef.current;
310
+ const endTime = actualEndTime !== undefined ? actualEndTime : currentTimeWrapper;
311
+
312
+ next(responseData, startTime ?? undefined, endTime ?? undefined);
313
+ };
314
+
315
+ return React.cloneElement(child, {
316
+ ...child.props,
317
+ next: interceptedNext,
318
+ });
319
+ }
320
+ return child;
321
+ });
322
+
323
+ return <>{childWithTimingProps}</>;
324
+ };
325
+
326
+ const currentInstruction = trialByteCode.instructions[instructionPointer];
327
+
328
+ let componentToRender = null;
329
+ if (currentInstruction?.type === 'ExecuteContent') {
330
+ const content = currentInstruction.content;
331
+ if (isRuntimeComponentContent(content)) {
332
+ const Component = componentsMap[content.type];
333
+
334
+ if (Component) {
335
+ const componentProps =
336
+ typeof content.props === 'function'
337
+ ? content.props(experimentStoreRef.current, data)
338
+ : content.props || {};
339
+
340
+ componentToRender = (
341
+ <TimingWrapper key={instructionPointer} collectRefreshRate={content.collectRefreshRate}>
342
+ <Component
343
+ next={next}
344
+ updateStore={updateStore}
345
+ store={experimentStoreRef.current}
346
+ data={data}
347
+ {...(content.type === 'Quest' ? { customQuestions: customQuestionsMap } : {})}
348
+ {...componentProps}
349
+ />
350
+ </TimingWrapper>
351
+ );
352
+ } else {
353
+ console.error(`No component found for type: ${content.type}`);
354
+ componentToRender = <div>Error: Component type "{content.type}" not found.</div>;
355
+ }
356
+ } else {
357
+ console.warn('ExecuteContent instruction does not contain standard component data:', content);
358
+ componentToRender = <div>Non-component content encountered.</div>;
359
+ }
360
+ } else if (instructionPointer >= trialByteCode.instructions.length) {
361
+ componentToRender = <></>;
362
+ }
363
+
364
+ return (
365
+ <div className='w-full'>
366
+ <div
367
+ className={` ${
368
+ config.showProgressBar ? '' : 'hidden '
369
+ } px-4 mt-4 sm:mt-12 max-w-2xl mx-auto flex-1 h-6 bg-gray-200 rounded-full overflow-hidden`}
370
+ >
371
+ <div
372
+ className={`h-full bg-gray-200 rounded-full duration-300 ${
373
+ progress > 0 ? ' border-black border-2' : ''
374
+ }`}
375
+ style={{
376
+ width: `${progress * 100}%`,
377
+ backgroundImage: `repeating-linear-gradient(
378
+ -45deg, #E5E7EB, #E5E7EB 10px, #D1D5DB 10px, #D1D5DB 20px
379
+ )`,
380
+ transition: 'width 300ms ease-in-out',
381
+ }}
382
+ />
383
+ </div>
384
+ {componentToRender}
385
+ </div>
386
+ );
387
+ }
@@ -0,0 +1,13 @@
1
+ export { default as Text } from './text';
2
+ export { default as PlainInput } from './plaininput';
3
+ export { default as ProlificEnding } from './prolificending';
4
+ export { default as MicCheck } from './microphonecheck';
5
+ export { default as Quest } from './quest';
6
+ export { default as Upload } from './upload';
7
+ export { default as EnterFullscreen } from './enterfullscreen';
8
+ export { default as ExitFullscreen } from './exitfullscreen';
9
+ export { default as ExperimentProvider } from './experimentprovider';
10
+ export { default as ExperimentRunner } from './experimentrunner';
11
+ export { default as RequestFilePermission } from './mobilefilepermission';
12
+ export { default as CanvasBlock } from './canvasblock';
13
+ export { default as CheckDevice } from './checkdevice';
@@ -3,28 +3,19 @@ import { useEffect, useState } from 'react';
3
3
  import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
4
4
  import { Capacitor } from '@capacitor/core';
5
5
 
6
-
7
- // currently needed for old android verions (<=10) because they need permissions to save data to the documents folder
8
6
  export default function RequestFilePermission({ next }: BaseComponentProps) {
9
- const [permissionChecking, setPermissionChecking] = useState(false);
7
+ const [permissionStatus, setPermissionStatus] = useState('checking');
10
8
 
11
9
  useEffect(() => {
12
- // Prevent multiple checks
13
- if (permissionChecking) return;
10
+ if (permissionStatus !== 'checking') return;
14
11
 
15
12
  const checkPermission = async () => {
16
- // Only run on Android
17
13
  if (getPlatform() !== 'mobile' || Capacitor.getPlatform() !== 'android') {
18
14
  next({});
19
15
  return;
20
16
  }
21
-
22
- setPermissionChecking(true);
23
17
 
24
18
  try {
25
- // Try to write a test file to check permissions
26
- // For Android 9-10, this will trigger permission request
27
- // For Android 11+, this should work with scoped storage
28
19
  await Filesystem.writeFile({
29
20
  path: 'permission_check.txt',
30
21
  data: 'Testing permissions',
@@ -32,35 +23,37 @@ export default function RequestFilePermission({ next }: BaseComponentProps) {
32
23
  encoding: Encoding.UTF8
33
24
  });
34
25
 
35
-
36
26
  try {
37
27
  await Filesystem.deleteFile({
38
28
  path: 'permission_check.txt',
39
29
  directory: Directory.Documents
40
30
  });
41
31
  } catch (e) {
42
-
32
+ console.log('Cleanup error:', e);
43
33
  }
44
34
 
45
- // Permission granted, proceed to next step
35
+ setPermissionStatus('granted');
46
36
  next({});
47
37
  } catch (error) {
48
38
  console.error('Permission denied or error:', error);
49
-
50
- setPermissionChecking(false);
39
+ setPermissionStatus('denied');
51
40
  }
52
41
  };
53
42
 
54
43
  checkPermission();
55
- }, [next, permissionChecking]);
44
+ }, [next, permissionStatus]);
45
+
46
+ if (permissionStatus === 'checking') {
47
+ return null;
48
+ }
56
49
 
57
50
  return (
58
51
  <div className="flex flex-col items-center justify-center gap-4 p-6 text-xl mt-16 px-10">
59
52
  <p>Storage permission is required to save your data.</p>
60
53
  <p>Please grant permission when prompted.</p>
61
- <p>If there is not popup, go to "Settings" {">"} "Apps" {">"} "BeSeK" {">"} "Permissions" {">"} "Storage" {">"} "Allow"</p>
54
+ <p>If there is no popup, go to "Settings" {">"} "Apps" {">"} "Name of the App" {">"} "Permissions" {">"} "Storage" {">"} "Allow"</p>
62
55
  <button
63
- onClick={() => setPermissionChecking(false)}
56
+ onClick={() => setPermissionStatus('checking')}
64
57
  className="mt-8 bg-white px-8 py-3 border-2 border-black font-bold text-black text-lg rounded-xl shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none"
65
58
  >
66
59
  Try Again
@@ -1,5 +1,5 @@
1
- import { BaseComponentProps } from "../mod";
2
- import { useState } from "react";
1
+ import { BaseComponentProps } from '../mod';
2
+ import { useState } from 'react';
3
3
 
4
4
  function PlainInput({
5
5
  content,
@@ -8,7 +8,7 @@ function PlainInput({
8
8
  next,
9
9
  updateStore,
10
10
  animate = false,
11
- storeupdate, // user defined function
11
+ storeupdate,
12
12
  placeholder = 'Enter your response here',
13
13
  }: BaseComponentProps & {
14
14
  content: React.ReactNode;
@@ -29,7 +29,7 @@ function PlainInput({
29
29
  if (storeupdate) {
30
30
  updateStore(storeupdate(inputValue));
31
31
  }
32
- next({value: inputValue});
32
+ next({ value: inputValue });
33
33
  };
34
34
 
35
35
  return (
@@ -43,11 +43,11 @@ function PlainInput({
43
43
 
44
44
  <div className={`mt-8 ${animate ? 'animate-slide-down opacity-0' : ''}`}>
45
45
  <input
46
- type="text"
46
+ type='text'
47
47
  value={inputValue}
48
48
  onChange={handleChange}
49
49
  placeholder={placeholder}
50
- className="w-full px-4 py-3 border-2 border-black rounded-xl text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
50
+ className='w-full px-4 py-3 border-2 border-black rounded-xl text-lg focus:outline-none focus:ring-2 focus:ring-blue-500'
51
51
  />
52
52
  </div>
53
53
 
@@ -68,5 +68,4 @@ function PlainInput({
68
68
  );
69
69
  }
70
70
 
71
-
72
- export default PlainInput;
71
+ export default PlainInput;
@@ -1,17 +1,23 @@
1
- import { BaseComponentProps, getParam } from '../utils/common';
1
+ import { BaseComponentProps, getParam, registerComponentParams } from '../utils/common';
2
2
  import Text from '../components/text';
3
3
 
4
+ registerComponentParams('ProlificEnding', [
5
+ { name: 'cc', defaultValue: '', type: 'string', description: 'Completion code of the Profilic experiment (more commonly supplied via the code)' },
6
+ ])
7
+
4
8
  export default function ProlificEnding({
5
9
  prolificCode,
10
+ data,
11
+ updateStore
6
12
  }: { prolificCode?: string } & BaseComponentProps) {
7
- let prolificCodeUsed = prolificCode ?? getParam('cc', undefined, 'string') ?? null;
13
+ let prolificCodeUsed = prolificCode ?? getParam('cc', '', 'string') ?? null;
8
14
 
9
15
  const content = (
10
16
  <div className='flex flex-col items-center'>
11
17
  <svg className='w-12 h-12' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
12
18
  <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M5 13l4 4L19 7' />
13
19
  </svg>
14
- <p className='text-center'>
20
+ <p className=''>
15
21
  Thank you! Your data has been successfully submitted. <br /> You can go back to Prolific and
16
22
  enter the code {prolificCodeUsed} to finish the study. Alternatively, you can click on this
17
23
  link:{' '}
@@ -26,5 +32,5 @@ export default function ProlificEnding({
26
32
  </div>
27
33
  );
28
34
 
29
- return <Text content={content} buttonText='' next={() => {}} />;
35
+ return <Text data={data} updateStore={updateStore} content={content} buttonText='' next={() => {}} />;
30
36
  }
@@ -4,14 +4,11 @@ import { ReactQuestionFactory, Survey, SurveyQuestionElementBase } from 'survey-
4
4
  import { ContrastLight } from 'survey-core/themes';
5
5
  import 'survey-core/defaultV2.min.css';
6
6
 
7
-
8
7
  type ComponentsMap = {
9
8
  [key: string]: ComponentType<any>;
10
9
  };
11
10
 
12
-
13
11
  const registerCustomQuestion = (name: string, component: ComponentType<any>) => {
14
-
15
12
  class CustomQuestionModel extends Question {
16
13
  getType() {
17
14
  return name;
@@ -21,13 +18,12 @@ const registerCustomQuestion = (name: string, component: ComponentType<any>) =>
21
18
  Serializer.addClass(
22
19
  name,
23
20
  [],
24
- function() {
21
+ function () {
25
22
  return new CustomQuestionModel('');
26
23
  },
27
- 'question'
24
+ 'question',
28
25
  );
29
26
 
30
- // Create the question wrapper
31
27
  class CustomQuestionWrapper extends SurveyQuestionElementBase {
32
28
  constructor(props: any) {
33
29
  super(props);
@@ -50,53 +46,53 @@ const registerCustomQuestion = (name: string, component: ComponentType<any>) =>
50
46
  }
51
47
  }
52
48
 
53
- // Register the React component
54
49
  ReactQuestionFactory.Instance.registerQuestion(name, (props) => {
55
50
  return createElement(CustomQuestionWrapper, props);
56
51
  });
57
52
  };
58
53
 
59
54
  // The main Quest component
60
- function Quest({ next, surveyJson, customQuestions = {} }: {
61
- next: (data: object) => void;
55
+ function Quest({
56
+ next,
57
+ surveyJson,
58
+ customQuestions = {},
59
+ }: {
60
+ next: (data: object) => void;
62
61
  surveyJson: object;
63
62
  customQuestions?: ComponentsMap;
64
63
  }) {
65
-
66
- Object.keys(customQuestions).forEach(name => {
64
+ Object.keys(customQuestions).forEach((name) => {
67
65
  registerCustomQuestion(name, customQuestions[name]);
68
66
  });
69
67
 
70
-
71
- const survey = new Model({
72
- ...surveyJson,
73
- css: { ...defaultV2Css, root: 'sd-root-modern custom-root' }
68
+ const survey = new Model({
69
+ ...surveyJson,
70
+ css: { ...defaultV2Css, root: 'sd-root-modern custom-root' },
74
71
  });
75
72
  survey.applyTheme(ContrastLight);
76
73
 
77
- const saveResults = useCallback((sender: any) => {
78
- const resultData = [];
79
- for (const key in sender.data) {
80
- const question = sender.getQuestionByName(key);
81
- if (question) {
82
- resultData.push({
83
- name: key,
84
- type: question.jsonObj.type,
85
- value: question.value,
86
- });
74
+ const saveResults = useCallback(
75
+ (sender: any) => {
76
+ const resultData: Record<string, any> = {};
77
+ for (const key in sender.data) {
78
+ const question = sender.getQuestionByName(key);
79
+ if (question) {
80
+ resultData[key] = question.value;
81
+ }
87
82
  }
88
- }
89
- next(resultData);
90
- }, [next]);
83
+ next(resultData);
84
+ },
85
+ [next],
86
+ );
91
87
 
92
88
  survey.onComplete.add(saveResults);
93
89
 
94
90
  return (
95
- <div className="max-w-4xl mx-auto px-4">
96
- <div className="absolute inset-0 -z-10 h-full w-full bg-white bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px]" />
91
+ <div className='max-w-4xl mx-auto px-4'>
92
+ <div className='absolute inset-0 -z-10 h-full w-full bg-white bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px]' />
97
93
  <Survey model={survey} />
98
94
  </div>
99
95
  );
100
96
  }
101
97
 
102
- export default Quest;
98
+ export default Quest;