@adriansteffan/reactive 0.0.26 → 0.0.28
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/dist/{mod-Dqf5zajq.js → mod-CjZm1Ta9.js} +11254 -10012
- package/dist/mod.d.ts +199 -49
- package/dist/reactive.es.js +27 -15
- package/dist/reactive.umd.js +37 -39
- package/dist/style.css +1 -1
- package/dist/{web-6wmUWZwq.js → web-B_I1xy51.js} +1 -1
- package/dist/{web-CnAMKrLX.js → web-DfpITFBR.js} +1 -1
- package/package.json +7 -2
- package/src/components/canvasblock.tsx +519 -0
- package/src/components/checkdevice.tsx +158 -0
- package/src/components/enterfullscreen.tsx +114 -31
- package/src/components/exitfullscreen.tsx +98 -21
- package/src/components/experimentprovider.tsx +34 -20
- package/src/components/experimentrunner.tsx +387 -0
- package/src/components/index.ts +13 -0
- package/src/components/mobilefilepermission.tsx +12 -19
- package/src/components/plaininput.tsx +7 -8
- package/src/components/prolificending.tsx +10 -4
- package/src/components/quest.tsx +27 -31
- package/src/components/settingsscreen.tsx +770 -0
- package/src/components/text.tsx +48 -3
- package/src/components/upload.tsx +218 -47
- package/src/mod.tsx +3 -12
- package/src/types/array.d.ts +6 -0
- package/src/utils/array.ts +113 -0
- package/src/utils/bytecode.ts +178 -0
- package/src/utils/common.ts +170 -39
- package/template/.env.template +2 -1
- package/template/src/{App.tsx → Experiment.tsx} +4 -4
- package/template/src/main.tsx +4 -4
- package/template/tsconfig.json +1 -0
- package/src/components/experiment.tsx +0 -371
package/src/components/text.tsx
CHANGED
|
@@ -1,19 +1,64 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { BaseComponentProps, now } from '../mod';
|
|
3
|
+
|
|
1
4
|
function Text({
|
|
2
5
|
content,
|
|
3
6
|
buttonText = 'Click me',
|
|
4
7
|
className = '',
|
|
5
8
|
next,
|
|
6
9
|
animate = false,
|
|
10
|
+
allowedKeys = false,
|
|
7
11
|
}: {
|
|
8
12
|
content: React.ReactNode;
|
|
9
13
|
buttonText?: string;
|
|
10
14
|
onButtonClick?: () => void;
|
|
11
15
|
className?: string;
|
|
12
|
-
next: (newData: object) => void;
|
|
13
16
|
animate?: boolean;
|
|
14
|
-
|
|
17
|
+
allowedKeys?: string[] | boolean;
|
|
18
|
+
} & BaseComponentProps) {
|
|
19
|
+
const startTimeRef = useRef<number>(0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
startTimeRef.current = now();
|
|
23
|
+
|
|
24
|
+
const handleKeyPress = (event: KeyboardEvent) => {
|
|
25
|
+
const keypressTime = now();
|
|
26
|
+
|
|
27
|
+
const isKeyAllowed =
|
|
28
|
+
allowedKeys === true || (Array.isArray(allowedKeys) && allowedKeys.includes(event.key));
|
|
29
|
+
|
|
30
|
+
if (isKeyAllowed) {
|
|
31
|
+
const reactionTime = keypressTime - startTimeRef.current;
|
|
32
|
+
|
|
33
|
+
next({
|
|
34
|
+
key: event.key,
|
|
35
|
+
time: keypressTime,
|
|
36
|
+
reactionTime: reactionTime,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (allowedKeys) {
|
|
42
|
+
window.addEventListener('keydown', handleKeyPress);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
if (allowedKeys) {
|
|
47
|
+
window.removeEventListener('keydown', handleKeyPress);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}, [next, allowedKeys]);
|
|
51
|
+
|
|
15
52
|
const handleClick = () => {
|
|
16
|
-
|
|
53
|
+
const clickTime = now();
|
|
54
|
+
|
|
55
|
+
const reactionTime = clickTime - startTimeRef.current;
|
|
56
|
+
|
|
57
|
+
next({
|
|
58
|
+
key: 'button',
|
|
59
|
+
time: clickTime,
|
|
60
|
+
reactionTime: reactionTime,
|
|
61
|
+
});
|
|
17
62
|
};
|
|
18
63
|
|
|
19
64
|
return (
|
|
@@ -2,7 +2,16 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { useMutation } from '@tanstack/react-query';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
import { post } from '../utils/request';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
BaseComponentProps,
|
|
7
|
+
FileUpload,
|
|
8
|
+
getParam,
|
|
9
|
+
getPlatform,
|
|
10
|
+
Platform,
|
|
11
|
+
registerComponentParams,
|
|
12
|
+
Store,
|
|
13
|
+
TrialData,
|
|
14
|
+
} from '../utils/common';
|
|
6
15
|
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
|
|
7
16
|
|
|
8
17
|
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
|
@@ -17,6 +26,93 @@ interface UploadResponse {
|
|
|
17
26
|
message?: string;
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
// TODO: deduplicate values with upload function below
|
|
30
|
+
registerComponentParams('Upload', [
|
|
31
|
+
{
|
|
32
|
+
name: 'upload',
|
|
33
|
+
defaultValue: true,
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
description: 'Upload the data at the end of the experiment?',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'download',
|
|
39
|
+
defaultValue: false,
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'Locally download the data at the end of the experiment?',
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
type DataObject = {
|
|
46
|
+
[key: string]: string | number | boolean | null | undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function escapeCsvValue(value: any): string {
|
|
50
|
+
if (value === null || value === undefined) {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stringValue = String(value);
|
|
55
|
+
|
|
56
|
+
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
|
57
|
+
const escapedValue = stringValue.replace(/"/g, '""');
|
|
58
|
+
return `"${escapedValue}"`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return stringValue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function convertArrayOfObjectsToCSV(data: DataObject[]): string {
|
|
65
|
+
if (!data || data.length === 0) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const headerSet = new Set<string>();
|
|
70
|
+
data.forEach((obj) => {
|
|
71
|
+
Object.keys(obj).forEach((key) => {
|
|
72
|
+
headerSet.add(key);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
const headers = Array.from(headerSet);
|
|
76
|
+
|
|
77
|
+
const headerRow = headers.map((header) => escapeCsvValue(header)).join(',');
|
|
78
|
+
|
|
79
|
+
const dataRows = data.map((obj) => {
|
|
80
|
+
return headers
|
|
81
|
+
.map((header) => {
|
|
82
|
+
const value = obj[header];
|
|
83
|
+
return escapeCsvValue(value);
|
|
84
|
+
})
|
|
85
|
+
.join(',');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return [headerRow, ...dataRows].join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function combineTrialsToCsv(
|
|
92
|
+
data: any[],
|
|
93
|
+
filename: string,
|
|
94
|
+
names: string[],
|
|
95
|
+
fun?: (obj: any) => any,
|
|
96
|
+
) {
|
|
97
|
+
const processedData = names
|
|
98
|
+
.flatMap((name) => {
|
|
99
|
+
const responseData = data.find((d) => d.name === name)?.responseData;
|
|
100
|
+
if (Array.isArray(responseData)) {
|
|
101
|
+
return responseData.map((i) => ({
|
|
102
|
+
block: name,
|
|
103
|
+
...i,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
})
|
|
108
|
+
.map((x) => (fun ? fun(x) : x));
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
filename,
|
|
112
|
+
encoding: 'utf8' as const,
|
|
113
|
+
content: convertArrayOfObjectsToCSV(processedData),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
20
116
|
|
|
21
117
|
interface FileBackend {
|
|
22
118
|
directoryExists(path: string): Promise<boolean>;
|
|
@@ -26,7 +122,7 @@ interface FileBackend {
|
|
|
26
122
|
|
|
27
123
|
const createElectronFileBackend = (): FileBackend => {
|
|
28
124
|
const electronAPI = (window as any).electronAPI;
|
|
29
|
-
|
|
125
|
+
|
|
30
126
|
return {
|
|
31
127
|
directoryExists: async (path: string): Promise<boolean> => {
|
|
32
128
|
const result = await electronAPI.directoryExists(path);
|
|
@@ -37,91 +133,86 @@ const createElectronFileBackend = (): FileBackend => {
|
|
|
37
133
|
},
|
|
38
134
|
saveFile: async (filename: string, content: string, directory: string): Promise<void> => {
|
|
39
135
|
await electronAPI.saveFile(filename, content, directory);
|
|
40
|
-
}
|
|
136
|
+
},
|
|
41
137
|
};
|
|
42
138
|
};
|
|
43
139
|
|
|
44
|
-
|
|
45
140
|
const createCapacitorFileBackend = (parentFolder?: string): FileBackend => {
|
|
46
|
-
|
|
47
141
|
const getPath = (path: string): string => {
|
|
48
142
|
if (!parentFolder) {
|
|
49
143
|
return path;
|
|
50
144
|
}
|
|
51
145
|
return path ? `${parentFolder}/${path}` : parentFolder;
|
|
52
146
|
};
|
|
53
|
-
|
|
147
|
+
|
|
54
148
|
return {
|
|
55
149
|
directoryExists: async (path: string): Promise<boolean> => {
|
|
56
150
|
try {
|
|
57
|
-
|
|
58
151
|
if (parentFolder) {
|
|
59
152
|
try {
|
|
60
153
|
await Filesystem.readdir({
|
|
61
154
|
path: parentFolder,
|
|
62
|
-
directory: Directory.Documents
|
|
155
|
+
directory: Directory.Documents,
|
|
63
156
|
});
|
|
64
157
|
} catch {
|
|
65
158
|
// Parent folder doesn't exist, so subpath doesn't exist either
|
|
66
159
|
return false;
|
|
67
160
|
}
|
|
68
161
|
}
|
|
69
|
-
|
|
162
|
+
|
|
70
163
|
const fullPath = getPath(path);
|
|
71
164
|
const result = await Filesystem.readdir({
|
|
72
165
|
path: fullPath,
|
|
73
|
-
directory: Directory.Documents
|
|
166
|
+
directory: Directory.Documents,
|
|
74
167
|
});
|
|
75
|
-
return result.files.length >= 0;
|
|
168
|
+
return result.files.length >= 0;
|
|
76
169
|
} catch {
|
|
77
170
|
return false;
|
|
78
171
|
}
|
|
79
172
|
},
|
|
80
|
-
|
|
173
|
+
|
|
81
174
|
createDirectory: async (path: string): Promise<void> => {
|
|
82
175
|
try {
|
|
83
|
-
|
|
84
176
|
if (parentFolder) {
|
|
85
177
|
try {
|
|
86
178
|
await Filesystem.mkdir({
|
|
87
179
|
path: parentFolder,
|
|
88
180
|
directory: Directory.Documents,
|
|
89
|
-
recursive: false
|
|
181
|
+
recursive: false,
|
|
90
182
|
});
|
|
91
183
|
} catch (e) {
|
|
92
184
|
// Parent directory might already exist, that's fine
|
|
93
185
|
}
|
|
94
186
|
}
|
|
95
|
-
|
|
187
|
+
|
|
96
188
|
const fullPath = getPath(path);
|
|
97
189
|
await Filesystem.mkdir({
|
|
98
190
|
path: fullPath,
|
|
99
191
|
directory: Directory.Documents,
|
|
100
|
-
recursive: true
|
|
192
|
+
recursive: true,
|
|
101
193
|
});
|
|
102
194
|
} catch (e) {
|
|
103
195
|
console.error('Error creating directory:', e);
|
|
104
196
|
throw e;
|
|
105
197
|
}
|
|
106
198
|
},
|
|
107
|
-
|
|
199
|
+
|
|
108
200
|
saveFile: async (filename: string, content: string, directory: string): Promise<void> => {
|
|
109
|
-
|
|
110
201
|
const fullDirectory = getPath(directory);
|
|
111
|
-
|
|
202
|
+
|
|
112
203
|
await Filesystem.writeFile({
|
|
113
204
|
path: `${fullDirectory}/${filename}`,
|
|
114
205
|
data: content,
|
|
115
206
|
directory: Directory.Documents,
|
|
116
207
|
encoding: 'utf8' as Encoding,
|
|
117
208
|
});
|
|
118
|
-
}
|
|
209
|
+
},
|
|
119
210
|
};
|
|
120
211
|
};
|
|
121
212
|
|
|
122
|
-
const getFileBackend = (parentDir?: string): { backend: FileBackend | null
|
|
213
|
+
const getFileBackend = (parentDir?: string): { backend: FileBackend | null; type: Platform } => {
|
|
123
214
|
const platform = getPlatform();
|
|
124
|
-
|
|
215
|
+
|
|
125
216
|
switch (platform) {
|
|
126
217
|
case 'desktop':
|
|
127
218
|
return { backend: createElectronFileBackend(), type: platform };
|
|
@@ -135,35 +226,45 @@ const getFileBackend = (parentDir?: string): { backend: FileBackend | null, type
|
|
|
135
226
|
// Function to generate a unique directory name
|
|
136
227
|
const getUniqueDirectoryName = async (
|
|
137
228
|
backend: FileBackend,
|
|
138
|
-
baseSessionId: string
|
|
229
|
+
baseSessionId: string,
|
|
139
230
|
): Promise<string> => {
|
|
140
231
|
let uniqueSessionID = baseSessionId;
|
|
141
|
-
|
|
232
|
+
|
|
142
233
|
if (await backend.directoryExists(uniqueSessionID)) {
|
|
143
234
|
let counter = 1;
|
|
144
235
|
uniqueSessionID = `${baseSessionId}_${counter}`;
|
|
145
|
-
|
|
236
|
+
|
|
146
237
|
while (await backend.directoryExists(uniqueSessionID)) {
|
|
147
238
|
counter++;
|
|
148
239
|
uniqueSessionID = `${baseSessionId}_${counter}`;
|
|
149
240
|
}
|
|
150
241
|
}
|
|
151
|
-
|
|
242
|
+
|
|
152
243
|
return uniqueSessionID;
|
|
153
244
|
};
|
|
154
245
|
|
|
246
|
+
type CSVBuilder = {
|
|
247
|
+
filename?: string;
|
|
248
|
+
trials?: string[];
|
|
249
|
+
fun?: (row: Record<string, any>) => Record<string, any>;
|
|
250
|
+
};
|
|
251
|
+
|
|
155
252
|
export default function Upload({
|
|
156
253
|
data,
|
|
157
254
|
next,
|
|
158
255
|
store,
|
|
159
256
|
sessionID,
|
|
160
257
|
generateFiles,
|
|
258
|
+
sessionCSVBuilder,
|
|
259
|
+
trialCSVBuilders,
|
|
161
260
|
uploadRaw = true,
|
|
162
261
|
autoUpload = false,
|
|
163
262
|
androidFolderName,
|
|
164
263
|
}: BaseComponentProps & {
|
|
165
264
|
sessionID?: string | null;
|
|
166
265
|
generateFiles: (sessionID: string, data: TrialData[], store?: Store) => FileUpload[];
|
|
266
|
+
sessionCSVBuilder: CSVBuilder;
|
|
267
|
+
trialCSVBuilders: CSVBuilder[];
|
|
167
268
|
uploadRaw: boolean;
|
|
168
269
|
autoUpload: boolean;
|
|
169
270
|
androidFolderName?: string;
|
|
@@ -212,19 +313,18 @@ export default function Upload({
|
|
|
212
313
|
document.body.removeChild(a);
|
|
213
314
|
URL.revokeObjectURL(url);
|
|
214
315
|
}, []);
|
|
215
|
-
|
|
216
|
-
|
|
316
|
+
|
|
217
317
|
const handleUpload = useCallback(async () => {
|
|
218
318
|
setUploadState('uploading');
|
|
219
|
-
|
|
319
|
+
|
|
220
320
|
if (uploadInitiatedRef.current) {
|
|
221
321
|
return;
|
|
222
322
|
}
|
|
223
|
-
|
|
323
|
+
|
|
224
324
|
uploadInitiatedRef.current = true;
|
|
225
|
-
|
|
325
|
+
|
|
226
326
|
const sessionIDUpload = sessionID ?? uuidv4();
|
|
227
|
-
|
|
327
|
+
|
|
228
328
|
const files: FileUpload[] = generateFiles ? generateFiles(sessionIDUpload, data, store) : [];
|
|
229
329
|
if (uploadRaw) {
|
|
230
330
|
files.push({
|
|
@@ -233,41 +333,112 @@ export default function Upload({
|
|
|
233
333
|
encoding: 'utf8',
|
|
234
334
|
});
|
|
235
335
|
}
|
|
236
|
-
|
|
336
|
+
|
|
337
|
+
if (sessionCSVBuilder) {
|
|
338
|
+
type ParamDetails = {
|
|
339
|
+
value?: any;
|
|
340
|
+
defaultValue: any;
|
|
341
|
+
};
|
|
342
|
+
let paramsDict: Record<string, any> = {};
|
|
343
|
+
const paramsSource: Record<string, ParamDetails | any> | undefined =
|
|
344
|
+
data?.[0]?.responseData?.params;
|
|
345
|
+
if (paramsSource && typeof paramsSource === 'object' && paramsSource !== null) {
|
|
346
|
+
paramsDict = Object.entries(paramsSource).reduce(
|
|
347
|
+
(
|
|
348
|
+
accumulator: Record<string, any>,
|
|
349
|
+
[paramName, paramDetails]: [string, ParamDetails | any],
|
|
350
|
+
) => {
|
|
351
|
+
if (
|
|
352
|
+
paramDetails &&
|
|
353
|
+
typeof paramDetails === 'object' &&
|
|
354
|
+
'defaultValue' in paramDetails
|
|
355
|
+
) {
|
|
356
|
+
const chosenValue = paramDetails.value ?? paramDetails.defaultValue;
|
|
357
|
+
accumulator[paramName] = chosenValue;
|
|
358
|
+
}
|
|
359
|
+
return accumulator;
|
|
360
|
+
},
|
|
361
|
+
{} as Record<string, any>,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let content = {
|
|
366
|
+
sessionID: sessionIDUpload,
|
|
367
|
+
userAgent: data[0].responseData.userAgent,
|
|
368
|
+
...paramsDict,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (
|
|
372
|
+
sessionCSVBuilder.trials &&
|
|
373
|
+
Array.isArray(sessionCSVBuilder.trials) &&
|
|
374
|
+
sessionCSVBuilder.trials.length > 0
|
|
375
|
+
) {
|
|
376
|
+
for (const trialName of sessionCSVBuilder.trials) {
|
|
377
|
+
const matchingDataElement = data.find((element) => element.name === trialName);
|
|
378
|
+
|
|
379
|
+
if (matchingDataElement?.responseData) {
|
|
380
|
+
if (
|
|
381
|
+
typeof matchingDataElement.responseData === 'object' &&
|
|
382
|
+
matchingDataElement.responseData !== null
|
|
383
|
+
) {
|
|
384
|
+
content = { ...content, ...matchingDataElement.responseData };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
files.push({
|
|
391
|
+
content: convertArrayOfObjectsToCSV([
|
|
392
|
+
sessionCSVBuilder.fun ? sessionCSVBuilder.fun(content) : content,
|
|
393
|
+
]),
|
|
394
|
+
filename: `${sessionIDUpload}${sessionCSVBuilder.filename}.csv`,
|
|
395
|
+
encoding: 'utf8' as const,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (trialCSVBuilders) {
|
|
400
|
+
for (const builder of trialCSVBuilders) {
|
|
401
|
+
files.push(
|
|
402
|
+
combineTrialsToCsv(
|
|
403
|
+
data,
|
|
404
|
+
`${sessionIDUpload}${builder.filename}.csv`,
|
|
405
|
+
builder.trials ?? [],
|
|
406
|
+
builder.fun,
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
237
412
|
try {
|
|
238
413
|
const payload: UploadPayload = {
|
|
239
414
|
sessionId: sessionIDUpload,
|
|
240
|
-
files,
|
|
415
|
+
files: files.map((file) => ({ ...file, encoding: file.encoding ?? 'utf8' })),
|
|
241
416
|
};
|
|
242
|
-
|
|
417
|
+
|
|
243
418
|
if (shouldDownload) {
|
|
244
419
|
await downloadFiles(files);
|
|
245
420
|
}
|
|
246
|
-
|
|
421
|
+
|
|
247
422
|
if (!shouldUpload) {
|
|
248
423
|
next({});
|
|
249
424
|
return;
|
|
250
425
|
}
|
|
251
|
-
|
|
426
|
+
|
|
252
427
|
// Get the current platform and appropriate file backend
|
|
253
428
|
const { backend, type } = getFileBackend(androidFolderName);
|
|
254
|
-
|
|
429
|
+
|
|
255
430
|
if (type === 'web') {
|
|
256
|
-
// Web API case
|
|
257
431
|
uploadData.mutate(payload);
|
|
258
432
|
} else if (backend) {
|
|
259
433
|
try {
|
|
260
|
-
// Get a unique directory name
|
|
261
434
|
const uniqueSessionID = await getUniqueDirectoryName(backend, sessionIDUpload);
|
|
262
|
-
|
|
263
|
-
// Create the directory
|
|
435
|
+
|
|
264
436
|
await backend.createDirectory(uniqueSessionID);
|
|
265
|
-
|
|
266
|
-
// Save all files
|
|
437
|
+
|
|
267
438
|
for (const file of files) {
|
|
268
439
|
await backend.saveFile(file.filename, file.content, uniqueSessionID);
|
|
269
440
|
}
|
|
270
|
-
|
|
441
|
+
|
|
271
442
|
setUploadState('success');
|
|
272
443
|
next({});
|
|
273
444
|
} catch (error) {
|
|
@@ -313,7 +484,7 @@ export default function Upload({
|
|
|
313
484
|
</p>
|
|
314
485
|
<button
|
|
315
486
|
onClick={handleUpload}
|
|
316
|
-
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'
|
|
487
|
+
className='mt-8 cursor-pointer 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'
|
|
317
488
|
>
|
|
318
489
|
Submit Data
|
|
319
490
|
</button>
|
|
@@ -335,7 +506,7 @@ export default function Upload({
|
|
|
335
506
|
</div>
|
|
336
507
|
<button
|
|
337
508
|
onClick={handleUpload}
|
|
338
|
-
className='px-4 py-2 bg-blue-500 text-white rounded-sm hover:bg-blue-600 transition-colors'
|
|
509
|
+
className='px-4 py-2 cursor-pointer bg-blue-500 text-white rounded-sm hover:bg-blue-600 transition-colors'
|
|
339
510
|
>
|
|
340
511
|
Try Again
|
|
341
512
|
</button>
|
package/src/mod.tsx
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import './index.css';
|
|
2
|
-
import Text from './components/text';
|
|
3
|
-
import PlainInput from './components/plaininput';
|
|
4
|
-
import ProlificEnding from './components/prolificending';
|
|
5
|
-
import MicCheck from './components/microphonecheck';
|
|
6
|
-
import Quest from './components/quest';
|
|
7
|
-
import Upload from './components/upload';
|
|
8
|
-
import EnterFullscreen from './components/enterfullscreen';
|
|
9
|
-
import ExitFullscreen from './components/exitfullscreen';
|
|
10
|
-
import ExperimentProvider from './components/experimentprovider';
|
|
11
|
-
import Experiment from './components/experiment';
|
|
12
|
-
import RequestFilePermission from './components/mobilefilepermission';
|
|
13
2
|
import { BaseComponentProps, ExperimentConfig } from './utils/common';
|
|
14
3
|
|
|
15
|
-
export { Text, ProlificEnding, MicCheck, Quest, Upload, EnterFullscreen, ExitFullscreen, Experiment, ExperimentProvider, PlainInput, RequestFilePermission};
|
|
16
4
|
export type { BaseComponentProps, ExperimentConfig };
|
|
5
|
+
|
|
6
|
+
export * from './utils/array';
|
|
17
7
|
export * from './utils/common';
|
|
8
|
+
export * from './components';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Array<T> {
|
|
3
|
+
/**
|
|
4
|
+
* Returns random elements from the array
|
|
5
|
+
* @param n Number of random elements to return (defaults to 1)
|
|
6
|
+
*/
|
|
7
|
+
sample(n?: number): Array<T>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shuffles array elements using Fisher-Yates algorithm
|
|
11
|
+
*/
|
|
12
|
+
shuffle(): Array<T>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Applies a function to the array
|
|
16
|
+
* @param fn Function to apply to the array
|
|
17
|
+
*/
|
|
18
|
+
pipe<U>(fn: (arr: Array<T>) => U): U;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Splits array into chunks
|
|
22
|
+
* @param n Number of chunks to create
|
|
23
|
+
*/
|
|
24
|
+
chunk(n: number): Array<Array<T>>;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns random elements from an array
|
|
30
|
+
*/
|
|
31
|
+
export function sample<T>(array: T[], n: number = 1): T[] {
|
|
32
|
+
const result: T[] = [];
|
|
33
|
+
|
|
34
|
+
if (array.length === 0) {
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const randomIndex = Math.floor(Math.random() * array.length);
|
|
40
|
+
result.push(array[randomIndex]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Shuffles array elements using Fisher-Yates algorithm
|
|
48
|
+
*/
|
|
49
|
+
export function shuffle<T>(array: T[]): T[] {
|
|
50
|
+
const result = [...array];
|
|
51
|
+
|
|
52
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
53
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
54
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Applies a function to an array
|
|
61
|
+
*/
|
|
62
|
+
export function pipe<T, U>(array: T[], fn: (arr: T[]) => U): U {
|
|
63
|
+
return fn(array);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Splits array into chunks
|
|
68
|
+
*/
|
|
69
|
+
export function chunk<T>(array: T[], n: number): T[][] {
|
|
70
|
+
const size = Math.ceil(array.length / n);
|
|
71
|
+
return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
|
|
72
|
+
array.slice(i * size, i * size + size),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Registers array methods on the Array prototype.
|
|
78
|
+
* Call this function once to make array methods available globally.
|
|
79
|
+
*
|
|
80
|
+
* Example:
|
|
81
|
+
* ```
|
|
82
|
+
* import { registerArrayExtensions } from '@adriansteffan/reactive/array';
|
|
83
|
+
* registerArrayExtensions();
|
|
84
|
+
*
|
|
85
|
+
* const myArray = [1, 2, 3, 4, 5];
|
|
86
|
+
* myArray.shuffle();
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function registerArrayExtensions(): void {
|
|
90
|
+
if (typeof Array.prototype.sample !== 'function') {
|
|
91
|
+
Array.prototype.sample = function <T>(this: T[], n?: number): T[] {
|
|
92
|
+
return sample(this, n);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof Array.prototype.shuffle !== 'function') {
|
|
97
|
+
Array.prototype.shuffle = function <T>(this: T[]): T[] {
|
|
98
|
+
return shuffle(this);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof Array.prototype.pipe !== 'function') {
|
|
103
|
+
Array.prototype.pipe = function <T, U>(this: T[], fn: (arr: T[]) => U): U {
|
|
104
|
+
return pipe(this, fn);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof Array.prototype.chunk !== 'function') {
|
|
109
|
+
Array.prototype.chunk = function <T>(this: T[], n: number): T[][] {
|
|
110
|
+
return chunk(this, n);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|